This commit is contained in:
Lanta 2025-04-15 17:12:33 +02:00
parent 1be9732279
commit 01aa843d0b
18 changed files with 4122 additions and 2393 deletions

160
demo.php
View file

@ -1,42 +1,48 @@
<?php <?php
// --- Configuration and Data Processing ---
$dataPoints = array(
array("y" => 25, "label" => "Sunday"),
array("y" => 15, "label" => "Monday"),
array("y" => 25, "label" => "Tuesday"),
array("y" => 5, "label" => "Wednesday"),
array("y" => 10, "label" => "Thursday"),
array("y" => 0, "label" => "Friday"),
array("y" => 20, "label" => "Saturday")
);
?>
<?php
$dataPointsPerPlayer = []; $dataPointsPerPlayer = [];
$chartData = [];
$encodedChartData = '[]'; // Default to empty JSON array
$dataError = '';
$directory = 'data/killstats'; $directory = 'data/killstats';
// Check if the directory exists // Check if the directory exists
if (!is_dir($directory)) { if (!is_dir($directory)) {
die("The directory $directory does not exist"); $dataError = "The directory '$directory' does not exist or is not accessible.";
} } else {
// Attempt to open the directory
// Open the directory $handle = @opendir($directory); // Use @ to suppress default warning on failure
if ($handle = opendir($directory)) { if ($handle === false) {
$dataError = "Could not open the directory '$directory'. Check permissions.";
} else {
// Loop through the directory // Loop through the directory
while (false !== ($entry = readdir($handle))) { while (false !== ($entry = readdir($handle))) {
if ($entry !== '.' && $entry !== '..') { // Skip '.' and '..' and non-JSON files
// Read each file if ($entry === '.' || $entry === '..' || pathinfo($entry, PATHINFO_EXTENSION) !== 'json') {
continue;
}
$filepath = $directory . '/' . $entry; $filepath = $directory . '/' . $entry;
$content = file_get_contents($filepath); $content = @file_get_contents($filepath); // Use @ to suppress warning on failure
if ($content === false) {
// Log or handle file read error if necessary, maybe skip the file
error_log("Could not read file: " . $filepath);
continue;
}
// Decode JSON data to PHP array // Decode JSON data to PHP array
$jsonArray = json_decode($content, true); $jsonArray = json_decode($content, true);
// Extract playername // Basic validation of JSON structure
if (is_array($jsonArray) && isset($jsonArray['stats']['playername'], $jsonArray['created'], $jsonArray['winplace'])) {
$playername = $jsonArray['stats']['playername']; $playername = $jsonArray['stats']['playername'];
$winplace = $jsonArray['winplace'];
$createdTimestamp = $jsonArray['created'];
// Process the date // Process the date
$date = new DateTime($jsonArray['created']); try {
$date = new DateTime($createdTimestamp);
$label = $date->format('m-d'); // Month-Day format $label = $date->format('m-d'); // Month-Day format
// Initialize player array if not existing // Initialize player array if not existing
@ -45,58 +51,96 @@ if ($handle = opendir($directory)) {
} }
// Add to dataPoints for this player // Add to dataPoints for this player
$dataPointsPerPlayer[$playername][] = array( $dataPointsPerPlayer[$playername][] = [
"y" => $jsonArray['winplace'], "y" => $winplace,
"label" => $label "label" => $label
); ];
} catch (Exception $e) {
// Log or handle date parsing error
error_log("Could not parse date '$createdTimestamp' in file: " . $filepath);
continue;
}
} else {
// Log or handle invalid JSON structure
error_log("Invalid JSON structure or missing keys in file: " . $filepath);
continue;
} }
} }
closedir($handle); closedir($handle);
}
// Now, $dataPointsPerPlayer is an array that contains an array of data points for each player // Prepare data for the chart if processing was successful
// You can access the data for a specific player like this: if (empty($dataError) && !empty($dataPointsPerPlayer)) {
// $playerData = $dataPointsPerPlayer['Lanta01']; foreach ($dataPointsPerPlayer as $player => $dataPoints) {
// Sort data points by label (date) for each player
usort($dataPoints, function($a, $b) {
return strcmp($a['label'], $b['label']);
});
// Output the array for debugging purposes
// At the end of your PHP script, where you previously printed the array
// Instead, we will encode the $dataPointsPerPlayer array into JSON
$chartData = [];
foreach ($dataPointsPerPlayer as $player => $dataPoints) {
$chartData[] = [ $chartData[] = [
'type' => 'line', 'type' => 'line',
'showInLegend' => true, 'showInLegend' => true,
'legendText' => $player, 'legendText' => htmlspecialchars($player), // Escape player name for legend
'dataPoints' => $dataPoints 'dataPoints' => $dataPoints
]; ];
}
// Encode the final chart data
$encodedChartData = json_encode($chartData, JSON_NUMERIC_CHECK);
if ($encodedChartData === false) {
$dataError = "Failed to encode chart data into JSON.";
$encodedChartData = '[]'; // Reset to empty array on encoding failure
}
} elseif (empty($dataError)) {
$dataError = "No valid data found in '$directory' to generate chart.";
}
}
} }
// Store the JSON encoded data in a PHP variable // Note: The original $dataPoints array with days of the week was unused, so it's removed.
$encodedChartData = json_encode($chartData, JSON_NUMERIC_CHECK);
// You can then pass this to your JavaScript code like this
echo "<script>var playerData = $encodedChartData;</script>";
?> ?>
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html> <html>
<head> <head>
<script> <meta charset="UTF-8">
window.onload = function () { <title>Player Win Place Over Time</title>
<script src="https://cdn.canvasjs.com/canvasjs.min.js"></script>
<style>
body { font-family: sans-serif; }
.chart-container { height: 400px; width: 95%; margin: 20px auto; }
.error-message { color: red; text-align: center; margin-top: 20px; }
</style>
</head>
<body>
<?php if ($dataError): ?>
<p class="error-message"><?php echo htmlspecialchars($dataError); ?></p>
<?php else: ?>
<div id="chartContainer" class="chart-container"></div>
<script>
window.onload = function () {
// Use the PHP-generated JSON data
var playerData = <?php echo $encodedChartData; ?>;
if (playerData && playerData.length > 0) {
var chart = new CanvasJS.Chart("chartContainer", { var chart = new CanvasJS.Chart("chartContainer", {
animationEnabled: true,
theme: "light2", // Optional theme
title: { title: {
text: "Win place by Player / per day" text: "Win Place by Player / Per Day"
}, },
axisY: { axisY: {
title: "Win Place" title: "Win Place",
reversed: true, // Lower win place is better
interval: 1 // Show integer ranks
},
axisX: {
title: "Date (MM-DD)",
// Consider adding valueFormatString if labels become too crowded
}, },
legend: { legend: {
cursor: "pointer", cursor: "pointer",
verticalAlign: "center",
horizontalAlign: "right",
dockInsidePlotArea: false,
itemclick: function (e) { itemclick: function (e) {
// Toggle data series visibility on legend item click // Toggle data series visibility on legend item click
if (typeof(e.dataSeries.visible) === "undefined" || e.dataSeries.visible) { if (typeof(e.dataSeries.visible) === "undefined" || e.dataSeries.visible) {
@ -110,13 +154,13 @@ window.onload = function () {
data: playerData // This will be an array of data series, one per player data: playerData // This will be an array of data series, one per player
}); });
chart.render(); chart.render();
} else {
// Display a message if no data is available to chart
document.getElementById("chartContainer").innerHTML = '<p style="text-align:center; padding-top: 50px;">No chart data available.</p>';
}
}
</script>
<?php endif; ?>
}
</script>
</head>
<body>
<div id="chartContainer" style="height: 370px; width: 100%;"></div>
<script src="https://cdn.canvasjs.com/canvasjs.min.js"></script>
</body> </body>
</html> </html>

View file

@ -1,83 +1,181 @@
$logprefix = Get-Date -Format "ddMMyyyy_HHmmss" # --- Script Setup ---
if ($PSScriptRoot.length -eq 0) { $logPrefix = Get-Date -Format "yyyyMMdd_HHmmss" # Use standard sortable format
$scriptroot = Get-Location # Determine script root directory reliably
if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
else { Write-Output "Script root identified as: $scriptRoot"
$scriptroot = $PSScriptRoot
}
$RelativeLogdir = Join-Path -Path $scriptroot -ChildPath "..\logs"
$logDir = (Resolve-Path -Path $RelativeLogdir).Path
Start-Transcript -Path "$logDir/report_new_matches_$logprefix.log" -Append # Define paths using Join-Path
. $scriptroot\..\includes\ps1\lockfile.ps1 $includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
new-lock -by "report_new_matches" $configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
write-output "Scriptroot: $scriptroot" $discordConfigPath = Join-Path -Path $scriptRoot -ChildPath "config.php" # Config is in the same dir
write-output "Scriptname: $($MyInvocation.MyCommand)" $dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
write-output "Script: $($MyInvocation.MyCommand.Path)" $logDir = Join-Path -Path $scriptRoot -ChildPath "..\logs" # Assuming logs dir relative to scriptroot parent
write-output "PSScriptroot: $PSScriptRoot" $playerMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "player_matches.json"
write-output "Logdir: $logDir"
$fileContent = Get-Content -Path "$scriptroot/../config/config.php" -Raw # Ensure Log directory exists
if (-not (Test-Path -Path $logDir -PathType Container)) {
# Use regex to match the apiKey value Write-Warning "Log directory not found at '$logDir'. Attempting to create."
if ($fileContent -match "\`$apiKey\s*=\s*\'([^\']+)\'") { try {
$apiKey = $matches[1] New-Item -Path $logDir -ItemType Directory -Force -ErrorAction Stop | Out-Null
} Write-Output "Successfully created log directory."
else { } catch {
Write-Output "API Key not found" # Log to console if transcript path fails
Write-Error "Failed to create log directory '$logDir'. Please check permissions. Error: $($_.Exception.Message)"
# Continue without transcript if log dir fails? Or exit? For now, continue.
}
} }
$headers = @{ # Start Transcript (use calculated $logDir)
$transcriptPath = Join-Path -Path $logDir -ChildPath "report_new_matches_$logPrefix.log"
try {
Start-Transcript -Path $transcriptPath -Append -ErrorAction Stop
Write-Output "Starting report_new_matches script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
Write-Output "Transcript logging to: $transcriptPath"
} catch {
Write-Error "Failed to start transcript at '$transcriptPath'. Error: $($_.Exception.Message)"
# Exit if transcript is critical? For now, continue.
}
# --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
if ($transcriptPath) { Stop-Transcript }
exit 1
}
. $lockFilePath
New-Lock -by "report_new_matches" -ErrorAction Stop # Stop if locking fails
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Configuration Loading ---
$apiKey = $null
$webhookUrl = $null
$webhookUrlLosers = $null
# Load API Key from main config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') { $apiKey = $matches[1]; Write-Verbose "API Key loaded." }
else { Write-Warning "API Key pattern not found in '$phpConfigPath'." }
} catch { Write-Warning "Failed to read '$phpConfigPath': $($_.Exception.Message)" }
} else { Write-Warning "Main config file not found at '$phpConfigPath'." }
# Load Webhook URLs from discord/config.php
if (Test-Path -Path $discordConfigPath -PathType Leaf) {
try {
$discordFileContent = Get-Content -Path $discordConfigPath -Raw -ErrorAction Stop
if ($discordFileContent -match '^\s*\$webhookurl\s*=\s*''([^'']+)''') { $webhookUrl = $matches[1]; Write-Verbose "Main webhook URL loaded." }
else { Write-Warning "Main webhook URL pattern not found in '$discordConfigPath'." }
if ($discordFileContent -match '^\s*\$webhookurl_losers\s*=\s*''([^'']+)''') { $webhookUrlLosers = $matches[1]; Write-Verbose "Losers webhook URL loaded." }
else { Write-Warning "Losers webhook URL pattern not found in '$discordConfigPath'." }
} catch { Write-Warning "Failed to read '$discordConfigPath': $($_.Exception.Message)" }
} else { Write-Warning "Discord config file not found at '$discordConfigPath'." }
# Validate required config
if (-not $apiKey) { Write-Error "API Key missing."; throw "Missing API Key" }
if (-not $webhookUrl) { Write-Error "Main Discord webhook URL missing."; throw "Missing Webhook URL" }
if (-not $webhookUrlLosers) { Write-Error "Losers Discord webhook URL missing."; throw "Missing Losers Webhook URL" }
# --- API Headers ---
$apiHeaders = @{
'accept' = 'application/vnd.api+json' 'accept' = 'application/vnd.api+json'
'Authorization' = "$apiKey" 'Authorization' = "Bearer $apiKey"
}
$fileContent = Get-Content -Path "$scriptroot/../discord/config.php" -Raw
# Use regex to match the apiKey value
if ($fileContent -match "\`$webhookurl\s*=\s*'([^']+)'") {
$webhookurl = $matches[1]
} else {
Write-Output "No web url found"
}
# Use regex to match the losers webhook url
if ($fileContent -match "\`$webhookurl_losers\s*=\s*'([^']+)'") {
$webhookurl_losers = $matches[1]
} else {
Write-Output "No losers web url found"
}
function send-discord {
param (
$content
)
$payload = [PSCustomObject]@{
content = $content
} }
Invoke-RestMethod -Uri $webhookurl -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json' # --- Helper Function for API Calls (Copied from update_clan_members.ps1) ---
} function Invoke-PubgApi {
param(
function send-discord-losers { [Parameter(Mandatory=$true)][string]$Uri, [Parameter(Mandatory=$true)][hashtable]$Headers,
param ( [int]$RetryCount = 1, [int]$RetryDelaySeconds = 61
$content
) )
$payload = [PSCustomObject]@{ for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
content = $content try {
Write-Verbose "API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
if ($null -ne $response) { Write-Verbose "API call successful."; return $response }
else { Write-Warning "API call to $Uri returned null."; return $null }
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping $RetryDelaySeconds sec..."; Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) { Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"; return $null }
else { Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"; return $null }
}
}
return $null
} }
Invoke-RestMethod -Uri $webhookurl_losers -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json'
}
$map_map = @{ # --- Helper Function for Telemetry Download/Parse ---
function Get-TelemetryData {
param([string]$TelemetryUrl)
if (-not $TelemetryUrl) { Write-Warning "Get-TelemetryData: No Telemetry URL provided."; return $null }
$telemetryFileName = $TelemetryUrl.Split('/')[-1]
$telemetryCacheFilePath = Join-Path -Path $telemetryCachePath -ChildPath $telemetryFileName
# Try cache first
if (Test-Path -Path $telemetryCacheFilePath -PathType Leaf) {
Write-Verbose "Loading telemetry from cache: $telemetryCacheFilePath"
try {
$telemetry = Get-Content -Path $telemetryCacheFilePath | ConvertFrom-Json -ErrorAction Stop
if ($null -ne $telemetry) { return $telemetry }
else { Write-Warning "Failed to parse cached telemetry: $telemetryCacheFilePath" }
} catch { Write-Warning "Error reading cached telemetry '$telemetryCacheFilePath': $($_.Exception.Message)" }
}
# Download if not cached or cache failed
Write-Output "Downloading telemetry: $TelemetryUrl"
try {
$webHeaders = @{ 'Accept-Encoding' = 'gzip' }
$response = Invoke-WebRequest -Uri $TelemetryUrl -Headers $webHeaders -UseBasicParsing -ErrorAction Stop
$telemetryContent = $response.Content
$telemetryContent | Out-File -FilePath $telemetryCacheFilePath -Encoding UTF8 -ErrorAction SilentlyContinue
$telemetry = $telemetryContent | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $telemetry) { Write-Warning "Failed to parse downloaded telemetry from $TelemetryUrl." }
return $telemetry
} catch {
$errorMessage = $_.Exception.Message
Write-Warning "Failed to download/save telemetry from $TelemetryUrl. Error: $errorMessage"
return $null
}
}
# --- Discord Sending Functions (with Error Handling) ---
function Send-DiscordMessage {
param([string]$Webhook, [string]$Content)
if (-not $Webhook -or -not $Content) { Write-Warning "Send-DiscordMessage: Missing Webhook or Content."; return }
$payload = @{ content = $Content } | ConvertTo-Json -Depth 3
try {
Invoke-RestMethod -Uri $Webhook -Method Post -Body $payload -ContentType 'application/json' -ErrorAction Stop
Write-Verbose "Successfully sent message to Discord."
} catch {
$errorMessage = $_.Exception.Message
Write-Error "Failed to send message to Discord ($Webhook). Error: $errorMessage"
}
}
function Send-DiscordWin($Content) { Send-DiscordMessage -Webhook $webhookUrl -Content $Content }
function Send-DiscordLoss($Content) { Send-DiscordMessage -Webhook $webhookUrlLosers -Content $Content }
# --- Map Definitions ---
$mapNameLookup = @{ # Renamed from $map_map
"Baltic_Main" = "Erangel" "Baltic_Main" = "Erangel"
"Chimera_Main" = "Paramo" "Chimera_Main" = "Paramo"
"Desert_Main" = "Miramar" "Desert_Main" = "Miramar"
"DihorOtok_Main" = "Vikendi" "DihorOtok_Main" = "Vikendi"
"Erangel_Main" = "Erangel" "Erangel_Main" = "Erangel" # Duplicate key, might be intentional?
"Heaven_Main" = "Haven" "Heaven_Main" = "Haven"
"Kiki_Main" = "Deston" "Kiki_Main" = "Deston"
"Range_Main" = "Camp Jackal" "Range_Main" = "Camp Jackal"
@ -85,266 +183,360 @@ $map_map = @{
"Summerland_Main" = "Karakin" "Summerland_Main" = "Karakin"
"Tiger_Main" = "Taego" "Tiger_Main" = "Taego"
"Neon_Main" = "Rondo" "Neon_Main" = "Rondo"
} }
try { # --- Load Player Matches Data ---
$player_matches = get-content "$scriptroot/../data/player_matches.json" | convertfrom-json -Depth 100 $playerMatchesData = $null
} if (Test-Path -Path $playerMatchesJsonPath -PathType Leaf) {
catch {
Write-Output 'Unable to read file exitin'
}
Write-Output $player_matches
Write-Output $new_win_matches
$new_win_matches = $player_matches[-1].new_win_matches
# Gebruik nu de lijst van nieuwe verloren matches uit het JSON-bestand
$new_loss_matches = $player_matches[-1].new_loss_matches
# Post verloren matches naar #losers kanaal
foreach ($lossid in $new_loss_matches) {
$lossmatch = $player_matches.player_matches | Where-Object { $_.id -eq $lossid }
if ($null -eq $lossmatch) { continue }
if ($lossmatch[0].gameMode -eq 'tdm') { continue }
# Fetch detailed match stats and telemetry for the loss
$loss_match_stats = $null
$loss_telemetry = $null
try { try {
$loss_match_stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/matches/$lossid" -Method GET -Headers $headers $playerMatchesData = Get-Content -Path $playerMatchesJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
$loss_telemetry = (invoke-webrequest @($lossmatch.telemetry_url)[0]).content | convertfrom-json | where-object { ($_._T -eq 'LOGPLAYERTAKEDAMAGE') -or ($_._T -eq 'LOGPLAYERKILLV2') } if ($null -eq $playerMatchesData -or -not ($playerMatchesData -is [array])) {
Write-Error "Invalid structure in '$playerMatchesJsonPath'. Expected array. Cannot proceed."
throw "Invalid player matches data."
}
Write-Output "Successfully loaded player matches data."
} catch { } catch {
$errorMessage = $_.Exception.Message Write-Error "Error reading '$playerMatchesJsonPath': $($_.Exception.Message). Cannot proceed."
Write-Warning ("Failed to fetch API/telemetry data for loss match {0}: {1}" -f $lossid, $errorMessage) throw $_
}
} else {
Write-Error "Player matches file not found at '$playerMatchesJsonPath'. Cannot proceed."
throw "Missing player matches file."
} }
$loss_stats_table = @() # --- Extract New Wins and Losses ---
$loss_victims = @() # For team damage # Find the special entries added by get_matches.ps1
$newWinEntry = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -eq 'new_win_matches' } | Select-Object -First 1
$newLossEntry = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -eq 'new_loss_matches' } | Select-Object -First 1
# Iterate through players found in the locally stored match data for this loss $newWinMatchIds = if ($null -ne $newWinEntry -and $newWinEntry.new_win_matches -is [array]) { $newWinEntry.new_win_matches } else { @() }
foreach ($player_stat in $lossmatch[0].stats) { $newLossMatchIds = if ($null -ne $newLossEntry -and $newLossEntry.new_loss_matches -is [array]) { $newLossEntry.new_loss_matches } else { @() }
$player_name = $player_stat.name
# Find the corresponding detailed stats from the API response
$detailed_player_stats = $null
if ($null -ne $loss_match_stats) {
$detailed_player_stats = $loss_match_stats.included | Where-Object {$_.type -eq 'participant'} | ForEach-Object {$_.attributes.stats} | Where-Object { $_.name -eq $player_name }
}
if ($null -eq $detailed_player_stats) { Write-Output "Found $($newWinMatchIds.Count) new win match IDs and $($newLossMatchIds.Count) new loss match IDs to report."
Write-Warning "Could not find detailed stats for player $player_name in loss match $lossid. Using basic stats."
# Fallback to basic stats if detailed stats are missing
$loss_stats_table += [PSCustomObject]@{
Name = $player_name
'Human dmg' = "N/A"
'Human Kills' = "N/A"
'Dmg' = "$([math]::Round($player_stat.damageDealt))" # Use basic stat
'Kills' = "$($player_stat.kills)" # Use basic stat
'alive' = "$([math]::Round(($player_stat.timeSurvived / 60)))" # Use basic stat
}
continue # Skip telemetry processing if detailed stats failed
}
# Calculate stats (similar to win stats calculation) # Extract actual match data (excluding the special entries)
$human_dmg = "N/A" $actualMatchEntries = $playerMatchesData | Where-Object { $_.PSObject.Properties.Name -ne 'new_win_matches' -and $_.PSObject.Properties.Name -ne 'new_loss_matches' }
$human_kills = "N/A"
if ($null -ne $loss_telemetry) {
try {
$human_dmg = [math]::Round(($loss_telemetry | Where-Object { $_._T -eq 'LOGPLAYERTAKEDAMAGE' -and $_.attacker.name -eq $player_name -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId } | Measure-Object -Property damage -Sum).Sum)
$human_kills = ($loss_telemetry | Where-Object { $_._T -eq 'LOGPLAYERKILLV2' -and $_.killer.name -eq $player_name -and $_.victim.accountId -notlike "ai.*" }).count
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Error processing telemetry stats for {0} in loss {1}: {2}" -f $player_name, $lossid, $errorMessage)
}
}
$loss_stats_table += [PSCustomObject]@{ # --- Process and Report New Losses ---
Name = $player_name Write-Output "Processing $($newLossMatchIds.Count) new losses..."
'Human dmg' = "$human_dmg" foreach ($lossId in $newLossMatchIds) {
'Human Kills' = "$human_kills" if (-not $lossId) { continue }
'Dmg' = "$([math]::Round($detailed_player_stats.damageDealt))" Write-Output "Processing loss match ID: $lossId"
'Kills' = "$($detailed_player_stats.kills)"
'alive' = "$([math]::Round(($detailed_player_stats.timeSurvived / 60)))"
}
# Calculate team damage # Find all player entries related to this loss match ID in the loaded data
if ($null -ne $loss_telemetry) { $lossMatchPlayerEntries = $actualMatchEntries.player_matches | Where-Object { $_.id -eq $lossId }
try { if ($null -eq $lossMatchPlayerEntries -or $lossMatchPlayerEntries.Count -eq 0) {
$teamdmg = $loss_telemetry | Where-Object { Write-Warning "Could not find match details for loss ID $lossId in player_matches.json data."
$_._T -eq 'LOGPLAYERTAKEDAMAGE' -and
$_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and
$_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $player_name
}
if ($teamdmg.count -ge 1) {
foreach ($victim_name in ($teamdmg.victim.name | Select-Object -Unique)) {
$loss_victims += [PSCustomObject]@{
attacker = $player_name
victim = $victim_name
Damage = "$([math]::Round((($teamdmg | Where-Object { $_.victim.name -eq $victim_name }).damage | Measure-Object -Sum).Sum))"
}
}
}
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Error processing team damage for {0} in loss {1}: {2}" -f $player_name, $lossid, $errorMessage)
}
}
}
# Format the stats table
$content_lossstats = ""
if ($loss_stats_table.Count -gt 0) {
$content_lossstats = '```' + ($loss_stats_table | Format-Table -AutoSize | Out-String) + '```'
}
# Format team damage table
$content_loss_victims = ""
if ($loss_victims.Count -gt 0) {
$content_loss_victims = ":skull::skull: Team Damage :skull::skull:`n" + '```' + ($loss_victims | Format-Table -AutoSize | Out-String) + '```'
}
# Original message construction variables
$losers = $lossmatch[0].stats.name -join ', ' # Join names for display
$map = $map_map[$lossmatch[0].mapName]
$place = ($lossmatch[0].stats | Select-Object -First 1).winPlace # Get placement from the first player stat
$first_player_name = ($lossmatch[0].stats | Select-Object -First 1).name
$replay_url = $lossmatch[0].telemetry_url -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg'
$replay_url = $replay_url -replace '-telemetry.json', ''
$replay_url = $replay_url + "?follow=$first_player_name" # Follow the first player
$match_settings = @"
``````
match mode $($lossmatch[0].gameMode)
match type $($lossmatch[0].matchType)
map $($map_map[$lossmatch[0].mapName])
id $($lossmatch[0].id)
``````
"@
send-discord-losers -content "We hebben een LOSERT! Geen Kip voor jou! :skull::skull:"
send-discord-losers -content ":partying_face::partying_face::partying_face: Helaas, $($losers) :partying_face::partying_face::partying_face:"
send-discord-losers -content $match_settings
send-discord-losers -content $content_lossstats
send-discord-losers -content $content_loss_victims
send-discord-losers -content "[2D replay](<$replay_url>)"
send-discord-losers -content "Meer match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$($lossmatch[0].id)>)"
}
foreach ($winid in $new_win_matches) {
$win_stats = @()
$victims = @()
if ($null -eq $winid) { continue }
$winmatches = $player_matches.player_matches | Where-Object { $_.id -eq $winid }
$telemetry = (invoke-webrequest @($winmatches.telemetry_url)[0]).content | convertfrom-json | where-object { ($_._T -eq 'LOGPLAYERTAKEDAMAGE') -or ($_._T -eq 'LOGPLAYERKILLV2') }
$winners = @(($winmatches | where-object { $_.stats.winPlace -eq 1 }).stats.name)
$2D_replay_url = @($winmatches.telemetry_url)[0] -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg'
$2D_replay_url = $2D_replay_url -replace '-telemetry.json', ''
$2D_replay_url = $2D_replay_url + "?follow=$($winners[0])"
$match_stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/matches/$winid" -Method GET -Headers $headers
if ($winmatches[0].gameMode -eq 'tdm' ) {
continue continue
} #skip tdm matches
if ($winmatches[0].matchType -eq 'custom') {
$players_to_report = $match_stats.included.attributes.stats | where-object { $_.playerId -notlike "ai.*" }
}
else {
$players_to_report = $match_stats.included.attributes.stats | where-object { $_.winplace -eq 1 }
} }
if ($new_win_matches.count -le 10) { # Use the first entry for common match details (assuming they are consistent)
#fail safe $firstLossEntry = $lossMatchPlayerEntries[0]
send-discord -content ":chicken: :chicken: **WINNER WINNER CHICKEN DINNER!!** :chicken: :chicken:" $lossGameMode = $firstLossEntry.gameMode
send-discord -content ":partying_face::partying_face::partying_face: Gefeliciteerd $($winners -join ', ') :partying_face::partying_face::partying_face:" $lossMatchType = $firstLossEntry.matchType
$match_settings = @" $lossMapNameRaw = $firstLossEntry.mapName
$lossTelemetryUrl = $firstLossEntry.telemetry_url
# Skip TDM matches
if ($lossGameMode -eq 'tdm') { Write-Output "Skipping TDM loss match $lossId."; continue }
# Fetch Full Match Stats from API (needed for all participants' details)
$lossMatchApiStats = Invoke-PubgApi -Uri "https://api.pubg.com/shards/steam/matches/$lossId" -Headers $apiHeaders
# Get Telemetry Data
$lossTelemetryEvents = Get-TelemetryData -TelemetryUrl $lossTelemetryUrl
$relevantLossTelemetry = $null
if ($lossTelemetryEvents -is [array]) {
$relevantLossTelemetry = $lossTelemetryEvents | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -or $_._T -eq 'LogPlayerKillV2' }
} else { Write-Warning "Invalid or missing telemetry data for loss match $lossId." }
# Prepare data tables
$lossStatsTable = @()
$lossTeamDamageVictims = @()
# Iterate through players involved in this loss from our data
$involvedPlayerNames = $lossMatchPlayerEntries.stats.name | Select-Object -Unique
foreach ($playerName in $involvedPlayerNames) {
$playerLossStats = $lossMatchPlayerEntries | Where-Object { $_.stats.name -eq $playerName } | Select-Object -First 1 -ExpandProperty stats
# Try to get more detailed stats from the full API response if available
$detailedPlayerStats = $null
if ($null -ne $lossMatchApiStats -and $lossMatchApiStats.included -is [array]) {
$detailedPlayerStats = $lossMatchApiStats.included |
Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.name -eq $playerName } |
Select-Object -First 1 -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
# Use detailed stats if found, otherwise fallback to basic stats from player_matches.json
$statsToUse = if ($null -ne $detailedPlayerStats) { $detailedPlayerStats } else { $playerLossStats }
if ($null -eq $statsToUse) { Write-Warning "Could not find any stats for $playerName in loss $lossId."; continue }
# Calculate Human Kills/Damage from Telemetry
$humanDmg = "N/A"
$humanKills = "N/A"
if ($null -ne $relevantLossTelemetry) {
try {
$humanDmgEvents = $relevantLossTelemetry | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -and $_.attacker.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId }
if ($humanDmgEvents) { $humanDmg = [math]::Round(($humanDmgEvents | Measure-Object -Property damage -Sum).Sum) }
$humanKillEvents = $relevantLossTelemetry | Where-Object { $_._T -eq 'LogPlayerKillV2' -and $_.killer.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" }
$humanKills = $humanKillEvents.Count
} catch { Write-Warning ("Error processing telemetry stats for {0} in loss {1}: {2}" -f $playerName, $lossId, $_.Exception.Message) }
}
# Add to stats table
$lossStatsTable += [PSCustomObject]@{
Name = $playerName
'Human dmg' = "$humanDmg"
'Human Kills' = "$humanKills"
'Dmg' = "$([math]::Round($statsToUse.damageDealt))"
'Kills' = "$($statsToUse.kills)"
'alive (min)' = "$([math]::Round(($statsToUse.timeSurvived / 60)))" # Clarify unit
}
# Calculate team damage from Telemetry
if ($null -ne $relevantLossTelemetry) {
try {
$teamDmgEvents = $relevantLossTelemetry | Where-Object {
$_._T -eq 'LogPlayerTakeDamage' -and $_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and $_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $playerName
}
if ($teamDmgEvents) {
foreach ($victimEntry in ($teamDmgEvents | Group-Object victim.name)) {
$lossTeamDamageVictims += [PSCustomObject]@{
attacker = $playerName
victim = $victimEntry.Name
Damage = "$([math]::Round(($victimEntry.Group.damage | Measure-Object -Sum).Sum))"
}
}
}
} catch { Write-Warning ("Error processing team damage for {0} in loss {1}: {2}" -f $playerName, $lossId, $_.Exception.Message) }
}
} # End foreach playerName in loss
# Format tables for Discord message
$contentLossStats = if ($lossStatsTable.Count -gt 0) { '```' + ($lossStatsTable | Format-Table -AutoSize | Out-String) + '```' } else { "" }
$contentLossVictims = if ($lossTeamDamageVictims.Count -gt 0) { ":skull::skull: Team Damage :skull::skull:`n" + '```' + ($lossTeamDamageVictims | Format-Table -AutoSize | Out-String) + '```' } else { "" }
# Construct and Send Loss Message
$losersString = $involvedPlayerNames -join ', '
$mapDisplayName = if ($mapNameLookup.ContainsKey($lossMapNameRaw)) { $mapNameLookup[$lossMapNameRaw] } else { $lossMapNameRaw }
$firstPlayerName = $involvedPlayerNames[0] # Use first player for replay link
$replayUrl = $lossTelemetryUrl -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg' -replace '-telemetry.json', ''
$replayUrl += "?follow=$firstPlayerName"
$matchSettings = @"
`````` ``````
match mode $($winmatches[0].gameMode) Match Mode : $lossGameMode
match type $($winmatches[0].matchType) Match Type : $lossMatchType
map $($map_map[$winmatches[0].mapName]) Map : $mapDisplayName
id $($winmatches[0].id) Match ID : $lossId
`````` ``````
"@ "@
send-discord -content $match_settings Send-DiscordLoss -Content "We hebben een LOSERT! Geen Kip voor jou! :skull::skull:"
} Send-DiscordLoss -Content ":partying_face::partying_face: Helaas, **$($losersString)** :partying_face::partying_face:"
else { Send-DiscordLoss -Content $matchSettings
write-output "Something went wrong (more then 10 matches to report)" if ($contentLossStats) { Send-DiscordLoss -Content $contentLossStats }
} if ($contentLossVictims) { Send-DiscordLoss -Content $contentLossVictims }
foreach ($player in $players_to_report.name) { Send-DiscordLoss -Content "[2D Replay](<$replayUrl>)"
if ($null -eq $player) { continue } Send-DiscordLoss -Content "Meer match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$lossId>)"
write-output "creating table for player $player"
$win_stats += [PSCustomObject]@{ Write-Output "Sent loss report for match $lossId."
Name = $player Start-Sleep -Seconds 2 # Small delay between messages
'Human dmg' = "$([math]::Round(($telemetry | Where-Object { $_._T -eq 'LOGPLAYERTAKEDAMAGE' -and $_.attacker.name -eq $player -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId } | Measure-Object -Property damage -Sum).Sum))"
'Human Kills' = "$(($telemetry | Where-Object { $_._T -eq 'LOGPLAYERKILLV2' -and $_.killer.name -eq $player -and $_.victim.accountId -notlike "ai.*" }).count)" } # End foreach lossId
'Dmg' = "$([math]::Round(($players_to_report | Where-Object { $_.name -eq $player }).damageDealt))"
'Kills' = "$(($players_to_report | Where-Object { $_.name -eq $player }).kills)" # --- Process and Report New Wins ---
'alive' = "$([math]::Round((($players_to_report | Where-Object { $_.name -eq $player }).timeSurvived /60 )))" Write-Output "Processing $($newWinMatchIds.Count) new wins..."
} foreach ($winId in $newWinMatchIds) {
$teamdmg = $telemetry | Where-Object { if (-not $winId) { continue }
$_._T -eq 'LOGPLAYERTAKEDAMAGE' -and Write-Output "Processing win match ID: $winId"
$_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and # Find all player entries related to this win match ID
$_.victim.name -ne $_.attacker.name -and $winMatchPlayerEntries = $actualMatchEntries.player_matches | Where-Object { $_.id -eq $winId }
$_.attacker.name -eq $player if ($null -eq $winMatchPlayerEntries -or $winMatchPlayerEntries.Count -eq 0) {
Write-Warning "Could not find match details for win ID $winId in player_matches.json data."
continue
} }
if ($teamdmg.count -ge 1) { # Use the first entry for common details
foreach ($victim in ($teamdmg.victim.name | Select-Object -Unique)) { $firstWinEntry = $winMatchPlayerEntries[0]
$victims += [PSCustomObject]@{ $winGameMode = $firstWinEntry.gameMode
attacker = $player $winMatchType = $firstWinEntry.matchType
victim = $victim $winMapNameRaw = $firstWinEntry.mapName
Damage = "$([math]::Round((($teamdmg | Where-Object { $_.victim.name -eq $victim }).damage | Measure-Object -Sum).Sum))" $winTelemetryUrl = $firstWinEntry.telemetry_url
# Skip TDM
if ($winGameMode -eq 'tdm') { Write-Output "Skipping TDM win match $winId."; continue }
# Get Telemetry
$winTelemetryEvents = Get-TelemetryData -TelemetryUrl $winTelemetryUrl
$relevantWinTelemetry = $null
if ($winTelemetryEvents -is [array]) {
$relevantWinTelemetry = $winTelemetryEvents | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -or $_._T -eq 'LogPlayerKillV2' }
} else { Write-Warning "Invalid or missing telemetry data for win match $winId." }
# Get Full Match Stats from API
$winMatchApiStats = Invoke-PubgApi -Uri "https://api.pubg.com/shards/steam/matches/$winId" -Headers $apiHeaders
# Determine players to report (winners or all non-AI in custom)
$playersToReportStats = @()
if ($null -ne $winMatchApiStats -and $winMatchApiStats.included -is [array]) {
if ($winMatchType -eq 'custom') {
$playersToReportStats = $winMatchApiStats.included | Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.playerId -notlike "ai.*" } | Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
} else {
$playersToReportStats = $winMatchApiStats.included | Where-Object { $_.type -eq 'participant' -and $_.attributes.stats.winPlace -eq 1 } | Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
} }
} else {
Write-Warning "Could not get full match stats from API for win $winId. Reporting might be incomplete."
# Fallback: Use players from our loaded data who won
$playersToReportStats = $winMatchPlayerEntries | Where-Object { $_.stats.winPlace -eq 1 } | Select-Object -ExpandProperty stats
} }
if ($playersToReportStats.Count -eq 0) { Write-Warning "No winning players found to report for match $winId."; continue }
$winnerNames = $playersToReportStats.name
$winnersString = $winnerNames -join ', '
# Prepare data tables
$winStatsTable = @()
$winTeamDamageVictims = @()
# Fail-safe check (from original script) - Limit number of reports if too many new wins detected at once?
# This might indicate an issue with the comparison logic or first run.
if ($newWinMatchIds.Count -gt 10) {
Write-Warning "More than 10 new win matches detected ($($newWinMatchIds.Count)). This might indicate an issue. Reporting only the first 10."
# Optionally break or limit the loop here if desired
# For now, just log the warning.
} }
} # Send initial win messages
write-output "New win matches:" Send-DiscordWin -Content ":chicken::chicken: **WINNER WINNER CHICKEN DINNER!!** :chicken::chicken:"
$new_win_matches Send-DiscordWin -Content ":partying_face::partying_face: Gefeliciteerd **$($winnersString)** :partying_face::partying_face:"
if ($new_win_matches.count -le 10) {
$content_winstats = '```' + ($win_stats | Format-Table | out-string) + '```'
send-discord -content $content_winstats
if ($victims.count -ge 1) { $mapDisplayName = if ($mapNameLookup.ContainsKey($winMapNameRaw)) { $mapNameLookup[$winMapNameRaw] } else { $winMapNameRaw }
send-discord -content ":skull::skull: Helaas hebben we deze keer ook team killers :skull::skull: " $matchSettings = @"
$content_victims = '```' + ($victims | Format-Table | out-string) + '```' ``````
send-discord -content $content_victims Match Mode : $winGameMode
Match Type : $winMatchType
Map : $mapDisplayName
Match ID : $winId
``````
"@
Send-DiscordWin -Content $matchSettings
# Calculate stats for each reported player
foreach ($playerStat in $playersToReportStats) {
$playerName = $playerStat.name
if (-not $playerName) { continue }
Write-Verbose "Creating stats table entry for winner $playerName in match $winId"
# Calculate Human Kills/Damage from Telemetry
$humanDmg = "N/A"
$humanKills = "N/A"
if ($null -ne $relevantWinTelemetry) {
try {
$humanDmgEvents = $relevantWinTelemetry | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -and $_.attacker.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId }
if ($humanDmgEvents) { $humanDmg = [math]::Round(($humanDmgEvents | Measure-Object -Property damage -Sum).Sum) }
$humanKillEvents = $relevantWinTelemetry | Where-Object { $_._T -eq 'LogPlayerKillV2' -and $_.killer.name -eq $playerName -and $_.victim.accountId -notlike "ai.*" }
$humanKills = $humanKillEvents.Count
} catch { Write-Warning ("Error processing telemetry stats for {0} in win {1}: {2}" -f $playerName, $winId, $_.Exception.Message) }
} }
send-discord -content "[2D replay](<$2D_replay_url>)" # Add to stats table
send-discord -content "More match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$($winmatches[0].id)>)" $winStatsTable += [PSCustomObject]@{
Name = $playerName
'Human dmg' = "$humanDmg"
'Human Kills' = "$humanKills"
'Dmg' = "$([math]::Round($playerStat.damageDealt))"
'Kills' = "$($playerStat.kills)"
'alive (min)' = "$([math]::Round(($playerStat.timeSurvived / 60)))"
} }
else {
write-output "Something went wrong (more then 10 matches to report)" # Calculate team damage from Telemetry
if ($null -ne $relevantWinTelemetry) {
try {
$teamDmgEvents = $relevantWinTelemetry | Where-Object {
$_._T -eq 'LogPlayerTakeDamage' -and $_.victim.teamId -eq $_.attacker.teamId -and
$_.victim.accountId -notlike "ai.*" -and $_.victim.name -ne $_.attacker.name -and
$_.attacker.name -eq $playerName
} }
$legenda = ' if ($teamDmgEvents) {
``` foreach ($victimEntry in ($teamDmgEvents | Group-Object victim.name)) {
dmg_h = Schade aangericht aan echte spelers $winTeamDamageVictims += [PSCustomObject]@{
dmg = Totale schade (aan zowel echte spelers als AI) attacker = $playerName
k_h = Aantal echte spelers die je hebt geelimineerd victim = $victimEntry.Name
K_a = Totale aantal eliminaties (inclusief AI) Damage = "$([math]::Round(($victimEntry.Group.damage | Measure-Object -Sum).Sum))"
t_serv = Overleefde tijd (in minuten) }
k_t = Team eliminaties }
``` }
' } catch { Write-Warning ("Error processing team damage for {0} in win {1}: {2}" -f $playerName, $winId, $_.Exception.Message) }
}
} # End foreach playerStat
#send-discord -content $legenda # Format and send stats tables
$contentWinStats = if ($winStatsTable.Count -gt 0) { '```' + ($winStatsTable | Format-Table -AutoSize | Out-String) + '```' } else { "" }
if ($contentWinStats) { Send-DiscordWin -Content $contentWinStats }
} $contentWinVictims = if ($winTeamDamageVictims.Count -gt 0) { ":skull::skull: Team Damage Report :skull::skull:`n" + '```' + ($winTeamDamageVictims | Format-Table -AutoSize | Out-String) + '```' } else { "" }
if ($contentWinVictims) { Send-DiscordWin -Content $contentWinVictims }
foreach ($item in $player_matches) { # Send Replay and Details Links
if ($item.PSObject.Properties.Name -contains "new_win_matches") { $firstWinnerName = $winnerNames[0]
$item.new_win_matches = $null $replayUrl = $winTelemetryUrl -replace 'https://telemetry-cdn.pubg.com/bluehole-pubg', 'https://chickendinner.gg' -replace '-telemetry.json', ''
$item.new_loss_matches = $null $replayUrl += "?follow=$firstWinnerName"
Send-DiscordWin -Content "[2D Replay](<$replayUrl>)"
Send-DiscordWin -Content "More match details [DTCH_STATS](<https://dtch.online/matchinfo.php?matchid=$winId>)"
Write-Output "Sent win report for match $winId."
Start-Sleep -Seconds 2 # Small delay between messages
} # End foreach winId
# --- Clear New Match Lists in Data File ---
Write-Output "Clearing new win/loss lists in player matches data file..."
$updatedPlayerMatchesData = $playerMatchesData # Start with the loaded data
$winListCleared = $false
$lossListCleared = $false
# Iterate through the array to find and modify the special entries
for ($i = 0; $i -lt $updatedPlayerMatchesData.Count; $i++) {
$item = $updatedPlayerMatchesData[$i]
if ($item -is [PSCustomObject]) {
if ($item.PSObject.Properties.Name -eq 'new_win_matches') {
$item.new_win_matches = @() # Clear the list
$winListCleared = $true
}
if ($item.PSObject.Properties.Name -eq 'new_loss_matches') {
$item.new_loss_matches = @() # Clear the list
$lossListCleared = $true
}
}
# Stop if both found and cleared
if ($winListCleared -and $lossListCleared) { break }
}
if ($winListCleared -or $lossListCleared) {
try {
$updatedPlayerMatchesData | ConvertTo-Json -Depth 100 | Out-File -FilePath $playerMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Successfully cleared new match lists in '$playerMatchesJsonPath'."
} catch {
Write-Error "Failed to save player matches data after clearing lists. '$playerMatchesJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Could not find 'new_win_matches' or 'new_loss_matches' entries to clear in '$playerMatchesJsonPath'."
}
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
if ($transcriptPath -and (Get-Transcript | Select-Object -ExpandProperty Path) -eq $transcriptPath) {
Stop-Transcript
} }
} }
# Convert back to JSON (optional)
$newJson = $player_matches | ConvertTo-Json -Depth 100
# Display the updated JSON
$newJson | out-file "$scriptroot/../data/player_matches.json"
remove-lock
Stop-Transcript

View file

@ -1,60 +1,139 @@
Start-Transcript -Path '/var/log/report_new_matches.log' -Append # --- Script Setup ---
# Note: Transcript path seems incorrect, should likely be specific to this script.
# Consider changing to '/var/log/dtch/report_to_discord.log' or similar.
Start-Transcript -Path '/var/log/dtch/report_to_discord.log' -Append
Write-Output "Starting report_to_discord script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
if ($PSScriptRoot.length -eq 0) { # Determine script root directory reliably
$scriptroot = Get-Location if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
else { Write-Output "Script root identified as: $scriptRoot"
$scriptroot = $PSScriptRoot
}
. $scriptroot\..\includes\ps1\lockfile.ps1
new-lock -by "report_to_discord"
function IsValidEntry($entry) {
return ($entry.KD_H -ne 'NaN' -and $entry.KD_ALL -ne 'NaN') -and
($entry.KD_H -ne 'Infinity' -and $entry.KD_ALL -ne 'Infinity')
}
$fileContent = Get-Content -Path "$scriptroot/../discord/config.php" -Raw
# Use regex to match the apiKey value # Define paths using Join-Path
if ($fileContent -match "\`$webhookurl\s*=\s*\'([^\']+)\'") { $includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$webhookurl = $matches[1] $discordConfigPath = Join-Path -Path $scriptRoot -ChildPath "config.php" # Config is in the same dir
} $dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
else { $lastStatsJsonPath = Join-Path -Path $dataPath -ChildPath "player_last_stats.json"
Write-Output "API Key not found"
}
$stats = get-content "$scriptroot/../data/player_last_stats.json" | convertfrom-json # --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
exit 1
}
. $lockFilePath
New-Lock -by "report_to_discord" -ErrorAction Stop # Stop if locking fails
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Helper Function ---
# Checks if K/D values are valid numbers (not NaN or Infinity)
function Test-IsValidKdEntry {
param($entry)
$filteredData = @{ # Check if properties exist and are numeric before comparison
all = $stats.all | Where-Object { IsValidEntry $_ } $kdHValid = $entry.PSObject.Properties.Name -contains 'KD_H' -and $entry.KD_H -is [double] -and -not [double]::IsNaN($entry.KD_H) -and -not [double]::IsInfinity($entry.KD_H)
} $kdAllValid = $entry.PSObject.Properties.Name -contains 'KD_ALL' -and $entry.KD_ALL -is [double] -and -not [double]::IsNaN($entry.KD_ALL) -and -not [double]::IsInfinity($entry.KD_ALL)
$most_kills = @{ return $kdHValid -and $kdAllValid
'name' = (($filteredData.all | Sort-Object kills -Descending)[0].playername) }
'stat' = (($filteredData.all | Sort-Object kills -Descending)[0].kills)
}
$most_deaths = @{
'name' = (($filteredData.all | Sort-Object deaths -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object deaths -Descending)[0].deaths)
}
$most_humankills = @{
'name' = (($filteredData.all | Sort-Object humankills -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object humankills -Descending)[0].humankills)
}
$most_KD_H = @{
'name' = (($filteredData.all | Sort-Object KD_H -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object KD_H -Descending)[0].KD_H)
}
$most_KD_ALL = @{
'name' = (($filteredData.all | Sort-Object KD_ALL -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object KD_ALL -Descending)[0].KD_ALL)
}
$most_matches = @{
'name' = (($filteredData.all | Sort-Object matches -Descending)[0].playername)
'stat' = (($filteredData.all | Sort-Object matches -Descending)[0].matches)
}
$content = " # --- Configuration Loading ---
$webhookUrl = $null
if (Test-Path -Path $discordConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $discordConfigPath -Raw -ErrorAction Stop
# Corrected regex for webhookurl
if ($fileContent -match '^\s*\$webhookurl\s*=\s*''([^'']+)''') {
$webhookUrl = $matches[1]
Write-Output "Discord webhook URL loaded successfully."
} else {
Write-Warning "Webhook URL pattern not found in '$discordConfigPath'."
}
} catch {
Write-Warning "Failed to read '$discordConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Discord config file not found at '$discordConfigPath'."
}
if (-not $webhookUrl) {
Write-Error "Discord webhook URL could not be loaded. Cannot send report."
throw "Missing Webhook URL" # Throw to trigger finally block
}
# --- Load Stats Data ---
$statsData = $null
if (Test-Path -Path $lastStatsJsonPath -PathType Leaf) {
try {
$statsData = Get-Content -Path $lastStatsJsonPath | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $statsData) {
Write-Error "Failed to parse stats data from '$lastStatsJsonPath'. Cannot generate report."
throw "Invalid Stats Data"
}
Write-Output "Successfully loaded player stats data."
} catch {
Write-Error "Error reading '$lastStatsJsonPath': $($_.Exception.Message). Cannot generate report."
throw $_
}
} else {
Write-Error "Player stats file not found at '$lastStatsJsonPath'. Cannot generate report."
throw "Missing Stats File"
}
# --- Process Stats ---
Write-Output "Processing stats..."
# Safely access the 'all' category and filter valid entries
$filteredAllStats = @()
if ($statsData.PSObject.Properties.Name -contains 'all' -and $statsData.all -is [array]) {
$filteredAllStats = $statsData.all | Where-Object { Test-IsValidKdEntry $_ }
Write-Output "Found $($filteredAllStats.Count) valid entries in 'all' category."
} else {
Write-Warning "Stats data does not contain a valid 'all' array. Report might be incomplete."
}
# --- Find Top Players (Handle Empty/Null Cases) ---
# Helper to safely get top player stat
function Get-TopStat {
param(
[array]$Data,
[string]$Property,
[string]$DefaultName = "N/A",
[string]$DefaultStat = "N/A"
)
$sorted = $Data | Sort-Object $Property -Descending -ErrorAction SilentlyContinue
if ($sorted -and $sorted.Count -gt 0) {
# Ensure the property exists on the top object before accessing
$topEntry = $sorted[0]
$playerName = if ($topEntry.PSObject.Properties.Name -contains 'playername') { $topEntry.playername } else { $DefaultName }
$statValue = if ($topEntry.PSObject.Properties.Name -contains $Property) { $topEntry.$Property } else { $DefaultStat }
# Format numeric stats if needed (e.g., K/D)
if ($Property -like "KD*" -and $statValue -is [double]) { $statValue = "{0:N2}" -f $statValue }
return @{ 'name' = $playerName; 'stat' = $statValue }
} else {
return @{ 'name' = $DefaultName; 'stat' = $DefaultStat }
}
}
$mostKills = Get-TopStat -Data $filteredAllStats -Property 'kills'
$mostDeaths = Get-TopStat -Data $filteredAllStats -Property 'deaths'
$mostHumanKills = Get-TopStat -Data $filteredAllStats -Property 'humankills'
$mostKdH = Get-TopStat -Data $filteredAllStats -Property 'KD_H'
$mostKdAll = Get-TopStat -Data $filteredAllStats -Property 'KD_ALL'
$mostMatches = Get-TopStat -Data $filteredAllStats -Property 'matches'
Write-Output "Determined top players for report."
# --- Format Discord Message ---
# Using PowerShell multi-line string with variable substitution
$reportContent = @"
:rocket: Het maandelijks raportje :rocket: :rocket: Het maandelijks raportje :rocket:
Hey toppers! Hey toppers!
@ -62,22 +141,22 @@ Hey toppers!
Laten we eens duiken in de cijfers van onze supergamers van de afgelopen maand: Laten we eens duiken in de cijfers van onze supergamers van de afgelopen maand:
:dart: Meeste Kills: :dart: Meeste Kills:
Hats off voor **$($most_kills['name'])**! Met **$($most_kills['stat'])** kills is hij/zij onze scherpschutter van de maand! Hats off voor **$($mostKills.name)**! Met **$($mostKills.stat)** kills is hij/zij onze scherpschutter van de maand!
:skull_crossbones: Meeste Deaths: :skull_crossbones: Meeste Deaths:
Oei, oei, oei... **$($most_deaths['name'])** is helaas het vaakst naar het hiernamaals gestuurd met **$($most_deaths['stat'])** deaths. Kop op, volgende keer beter! Oei, oei, oei... **$($mostDeaths.name)** is helaas het vaakst naar het hiernamaals gestuurd met **$($mostDeaths.stat)** deaths. Kop op, volgende keer beter!
:robot: Meeste Humankills: :robot: Meeste Humankills:
Watch out! We hebben een Terminator onder ons. Hoedje af voor **$($most_humankills['name'])** met **$($most_humankills['stat'])** humankills! Watch out! We hebben een Terminator onder ons. Hoedje af voor **$($mostHumanKills.name)** met **$($mostHumanKills.stat)** humankills!
:bar_chart: Beste KD Ratio (Alle vijanden): :bar_chart: Beste KD Ratio (Alle vijanden):
De onevenaarbare **$($most_KD_ALL['name'])** heeft een KD van **$($most_KD_ALL['stat'])** ! Niet slecht, toch? 😉 De onevenaarbare **$($mostKdAll.name)** heeft een KD van **$($mostKdAll.stat)** ! Niet slecht, toch? 😉
:adult: Beste KD Ratio (Alleen menselijke spelers): :adult: Beste KD Ratio (Alleen menselijke spelers):
Opgelet, gamers! **$($most_KD_H['name'])** heeft een KD van **$($most_KD_H['stat'])** tegen andere spelers! Wie daagt hem/haar uit? Opgelet, gamers! **$($mostKdH.name)** heeft een KD van **$($mostKdH.stat)** tegen andere spelers! Wie daagt hem/haar uit?
:video_game: Meeste Matches: :video_game: Meeste Matches:
Onze meest toegewijde gamer, **$($most_matches['name'])**, heeft maar liefst **$($most_matches['stat'])** matches gespeeld. Ga zo door! Onze meest toegewijde gamer, **$($mostMatches.name)**, heeft maar liefst **$($mostMatches.stat)** matches gespeeld. Ga zo door!
Da's het voor nu, gamers! Blijf schieten, blijf lachen en tot het volgende rapportje! Da's het voor nu, gamers! Blijf schieten, blijf lachen en tot het volgende rapportje!
@ -85,18 +164,33 @@ High fives en knuffels (virtueel, natuurlijk),
Het Gaming Team Het Gaming Team
Meer stats zijn hier te vinden : https://lanta.eu/DTCH Meer stats zijn hier te vinden : https://lanta.eu/DTCH
" "@
$content Write-Output "Formatted Discord message content."
# Write-Verbose $reportContent # Uncomment to see the full message in verbose logs
# --- Send to Discord ---
$payload = @{
content = $reportContent
# username = "Stats Bot" # Optional: Override bot name
# avatar_url = "" # Optional: Override bot avatar
} | ConvertTo-Json -Depth 3 # Depth 3 should be sufficient
Write-Output "Sending report to Discord webhook..."
try {
Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $payload -ContentType 'application/json' -ErrorAction Stop
Write-Output "Report successfully sent to Discord."
} catch {
$errorMessage = $_.Exception.Message
Write-Error "Failed to send report to Discord. Error: $errorMessage"
# Consider logging the failed payload or response details if available
# Write-Warning "Payload: $payload"
}
$payload = [PSCustomObject]@{ } # End Main Try Block
finally {
content = $content # --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
Stop-Transcript
} }
Invoke-RestMethod -Uri $webhookurl -Method Post -Body ($payload | ConvertTo-Json) -ContentType 'Application/Json'
remove-lock
Stop-Transcript

View file

@ -1,255 +1,567 @@
import json # -*- coding: utf-8 -*-
"""
Discord bot for team creation, stats reporting, and event logging.
"""
import json
import os import os
import discord import discord
import random import random
import asyncio import asyncio
import re import re
import logging
from discord.ext import commands from discord.ext import commands
# --- Configuration ---
def get_token(): # TODO: Move hardcoded channel names to a config file or environment variables
with open("config.php", "r") as file: TEAMIFY_VOICE_CHANNEL = "teamify"
TEAMIFY_TEXT_CHANNEL = "teamify"
TEMP_CATEGORY_NAME = "Temporary Teams"
LOGGING_CHANNEL = "logging"
WELCOME_CHANNEL = "raadhuisplein"
GOD_CHANNEL = "GOD_CHANNEL" # Channel to ignore for voice logging
# TODO: Move stats file path to config
STATS_FILE_PATH = os.path.join("..", "data", "player_last_stats.json")
CONFIG_PHP_PATH = "config.php" # Path relative to this script's location
# --- Logging Setup ---
# Basic logging to console
logging.basicConfig(level=logging.INFO, format='%(asctime)s:%(levelname)s:%(name)s: %(message)s')
logger = logging.getLogger('discord_bot')
# --- Helper Functions ---
def get_token_from_php(config_path):
"""
Reads the bot token from a PHP config file.
WARNING: This method of storing secrets is insecure.
Consider using environment variables or a dedicated secrets management solution.
"""
try:
with open(config_path, "r", encoding="utf-8") as file:
content = file.read() content = file.read()
match = re.search(r"bottoken\s*=\s*'(.+?)'", content) # Regex to find $bottoken = 'TOKEN_VALUE';
match = re.search(r"\$bottoken\s*=\s*'(.+?)'", content)
if match: if match:
logger.info("Successfully extracted bot token from %s", config_path)
return match.group(1) return match.group(1)
else:
logger.error("Bot token pattern not found in %s", config_path)
return None
except FileNotFoundError:
logger.error("Config file not found at %s", config_path)
return None
except Exception as e:
logger.exception("Error reading or parsing config file %s: %s", config_path, e)
return None return None
token = get_token() async def cleanup_empty_channels(guild, temp_channels, ctx_channel):
if not token: """
raise ValueError("Bot token niet gevonden in config.php") Periodically checks and removes empty temporary voice channels.
"""
while temp_channels:
await asyncio.sleep(60) # Check every 60 seconds
for channel in temp_channels[:]: # Iterate over a copy
try:
# Refresh channel state
current_channel = guild.get_channel(channel.id)
if current_channel and isinstance(current_channel, discord.VoiceChannel):
if len(current_channel.members) == 0:
logger.info("Deleting empty temporary channel: %s", current_channel.name)
await current_channel.delete(reason="Channel empty")
temp_channels.remove(channel)
# Optionally notify in the original context channel
# await ctx_channel.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
elif current_channel is None:
# Channel might have been deleted manually
logger.warning("Temporary channel %s (ID: %d) not found, removing from tracking list.", channel.name, channel.id)
temp_channels.remove(channel)
# Intents instellen except discord.Forbidden:
logger.error("Missing permissions to delete channel %s", channel.name)
# Remove from list to avoid repeated attempts if permissions are missing
temp_channels.remove(channel)
except discord.NotFound:
logger.warning("Attempted to delete channel %s but it was already gone.", channel.name)
if channel in temp_channels:
temp_channels.remove(channel)
except Exception as e:
logger.exception("Error during temporary channel cleanup for %s: %s", channel.name, e)
# Remove from list to avoid potential infinite loops on persistent errors
if channel in temp_channels:
temp_channels.remove(channel)
# --- Bot Setup ---
token = get_token_from_php(CONFIG_PHP_PATH)
if not token:
logger.critical("Bot token not found or could not be read. Exiting.")
exit() # Exit if token is essential
# Define Intents
intents = discord.Intents.default() intents = discord.Intents.default()
intents.voice_states = True intents.voice_states = True
intents.guilds = True intents.guilds = True
intents.messages = True intents.messages = True
intents.message_content = True intents.message_content = True
intents.presences = True # Nodig als de bot presences moet zien intents.presences = True # Required if the bot needs to see presences
intents.members = True # Nodig om leden in een voice channel te zien intents.members = True # Required to see members in voice channels and for join/remove events
bot = commands.Bot(command_prefix="!", intents=intents) bot = commands.Bot(command_prefix="!", intents=intents)
# --- Events ---
@bot.event @bot.event
async def on_ready(): async def on_ready():
print(f'Bot is ingelogd als {bot.user}') """Called when the bot is ready and connected."""
logger.info('Bot is ingelogd als %s (ID: %s)', bot.user, bot.user.id)
print(f'Bot is ingelogd als {bot.user}') # Keep console output for convenience
@bot.event
async def on_command_error(ctx, error):
"""Global command error handler."""
if isinstance(error, commands.CommandNotFound):
# Optionally ignore 'Command not found' errors or send a message
# await ctx.send("Onbekend commando.")
logger.warning("Command not found: %s", ctx.message.content)
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send(f"Je mist een argument: {error.param.name}")
elif isinstance(error, commands.BadArgument):
await ctx.send("Ongeldig argument opgegeven.")
elif isinstance(error, commands.CheckFailure):
await ctx.send("Je hebt geen permissies voor dit commando of kanaal.")
else:
# Log other errors
logger.exception("Unhandled command error in '%s': %s", ctx.command, error)
try:
# Attempt to notify user about unexpected error
await ctx.send("Er is een onverwachte fout opgetreden.")
except discord.HTTPException:
pass # Ignore if we can't send the error message
@bot.event
async def on_voice_state_update(member, before, after):
"""Logs user movements between voice channels."""
# Ignore bots
if member.bot:
return
logging_channel = discord.utils.get(member.guild.text_channels, name=LOGGING_CHANNEL)
if not logging_channel:
logger.warning("Logging channel '%s' not found.", LOGGING_CHANNEL)
return
# Ignore movements involving the GOD_CHANNEL
if (before.channel and before.channel.name == GOD_CHANNEL) or \
(after.channel and after.channel.name == GOD_CHANNEL):
return
try:
if before.channel is None and after.channel is not None:
# Member joins a voice channel
logger.info("%s joined voice channel: %s", member.display_name, after.channel.name)
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
elif before.channel is not None and after.channel is None:
# Member leaves a voice channel
logger.info("%s left voice channel: %s", member.display_name, before.channel.name)
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
elif before.channel != after.channel:
# Member switches voice channels
logger.info("%s switched from %s to %s", member.display_name, before.channel.name, after.channel.name)
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
except discord.Forbidden:
logger.error("Missing permissions to send message in logging channel '%s'", LOGGING_CHANNEL)
except Exception as e:
logger.exception("Error during on_voice_state_update: %s", e)
@bot.event
async def on_member_join(member):
"""Welcomes new members."""
if member.bot: return # Ignore bots
welcome_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL)
if welcome_channel:
logger.info("Member joined: %s", member.display_name)
try:
await welcome_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
except discord.Forbidden:
logger.error("Missing permissions to send message in welcome channel '%s'", WELCOME_CHANNEL)
except Exception as e:
logger.exception("Error during on_member_join: %s", e)
else:
logger.warning("Welcome channel '%s' not found.", WELCOME_CHANNEL)
@bot.event
async def on_member_remove(member):
"""Logs when members leave."""
if member.bot: return # Ignore bots
goodbye_channel = discord.utils.get(member.guild.text_channels, name=WELCOME_CHANNEL) # Using same channel as welcome
if goodbye_channel:
logger.info("Member left: %s", member.display_name)
try:
await goodbye_channel.send(f"😢 {member.display_name} heeft de server verlaten. We zullen je missen!") # Use display_name as mention won't work
except discord.Forbidden:
logger.error("Missing permissions to send message in goodbye channel '%s'", WELCOME_CHANNEL)
except Exception as e:
logger.exception("Error during on_member_remove: %s", e)
else:
logger.warning("Goodbye channel '%s' not found.", WELCOME_CHANNEL)
# --- Commands ---
@bot.command() @bot.command()
async def test(ctx): async def test(ctx):
"""A simple test command."""
await ctx.send("Test geslaagd!") await ctx.send("Test geslaagd!")
@bot.command() @bot.command()
async def teamify(ctx, *args): async def teamify(ctx, *args):
for arg in args: """
if arg.lower() == "help": Splits members in the 'teamify' voice channel into random teams.
Usage:
!teamify - Auto-split into teams of 4.
!teamify <num_teams> - Split into a specific number of teams.
!teamify move - Auto-split and move to temporary channels.
!teamify <num_teams> move - Split into specific teams and move.
!teamify help - Show help message.
"""
# Handle help argument first
if "help" in [arg.lower() for arg in args]:
help_message = ( help_message = (
"**Gebruik van !teamify:**\n" "**Gebruik van !teamify:**\n"
"`!teamify` - Verdeel spelers in teams van max. 4 personen.\n" f"`!teamify` - Verdeel spelers in `{TEAMIFY_VOICE_CHANNEL}` in teams van max. 4.\n"
"`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n" "`!teamify <aantal_teams>` - Verdeel spelers in een opgegeven aantal teams.\n"
"`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze naar tijdelijke voice-kanalen.\n" "`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke kanalen.\n"
"`!teamify move` - Verdeel spelers automatisch en verplaats ze naar tijdelijke voice-kanalen." "`!teamify <aantal_teams> move` - Verdeel spelers en verplaats ze.\n"
"`!teamify help` - Toon dit bericht."
) )
await ctx.send(help_message) await ctx.send(help_message)
return return
# Beperk het commando tot alleen het kanaal "teamify" # Check if command is used in the correct text channel
if ctx.channel.name != "teamify": if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify kanaal.") await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} kanaal.")
return return
guild = ctx.guild guild = ctx.guild
voice_channel = discord.utils.get(guild.voice_channels, name="teamify") source_voice_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
if not voice_channel or len(voice_channel.members) == 0: if not source_voice_channel:
await ctx.send("Er zijn geen mensen in het kanaal 'teamify' om teams van te maken!") await ctx.send(f"Voice kanaal '{TEAMIFY_VOICE_CHANNEL}' niet gevonden!")
return return
members = voice_channel.members if not source_voice_channel.members:
await ctx.send(f"Er zijn geen mensen in het kanaal '{TEAMIFY_VOICE_CHANNEL}' om teams van te maken!")
return
members = list(source_voice_channel.members) # Get a mutable list
random.shuffle(members) random.shuffle(members)
member_count = len(members)
# Standaardwaarden # Parse arguments
num_teams = 0 num_teams_arg = 0
move_players = False move_players = False
# Verwerk argumenten
for arg in args: for arg in args:
if arg.isdigit(): # Als het een getal is, gebruik het als het aantal teams if arg.isdigit():
num_teams = int(arg) num_teams_arg = int(arg)
elif arg.lower() == "move": # Als 'true' is opgegeven, verplaats spelers elif arg.lower() == "move":
move_players = True move_players = True
# Bepaal het aantal teams als niet opgegeven # Determine number of teams
if num_teams <= 0: if num_teams_arg <= 0:
num_teams = len(members) // 4 if len(members) >= 4 else 1 # Auto-calculate based on team size 4, minimum 1 team
num_teams = (member_count + 3) // 4 if member_count > 0 else 1
else:
num_teams = num_teams_arg
num_teams = min(num_teams, len(members)) # Ensure num_teams is valid
num_teams = max(1, min(num_teams, member_count)) # At least 1 team, max members count
# Create teams
teams = [[] for _ in range(num_teams)] teams = [[] for _ in range(num_teams)]
for i, member in enumerate(members): for i, member in enumerate(members):
teams[i % num_teams].append(member) teams[i % num_teams].append(member)
# Zoek het tekstkanaal "teamify" # Find the output text channel
text_channel = discord.utils.get(guild.text_channels, name="teamify") output_text_channel = discord.utils.get(guild.text_channels, name=TEAMIFY_TEXT_CHANNEL)
if not text_channel: if not output_text_channel:
await ctx.send("Het kanaal 'teamify' bestaat niet!") # Fallback to context channel if specific channel not found
return logger.warning("Output text channel '%s' not found, using context channel.", TEAMIFY_TEXT_CHANNEL)
output_text_channel = ctx.channel
message = f"Willekeurige teams uit {voice_channel.name}:\n\n" # await ctx.send(f"Tekst kanaal '{TEAMIFY_TEXT_CHANNEL}' bestaat niet! Resultaten worden hier gepost.")
category = discord.utils.get(guild.categories, name="Temporary Teams")
if not category:
category = await guild.create_category("Temporary Teams")
# Prepare message and temporary channels if moving
message = f"Willekeurige teams uit {source_voice_channel.mention}:\n\n"
temp_channels = [] temp_channels = []
category = None
if move_players:
try:
category = discord.utils.get(guild.categories, name=TEMP_CATEGORY_NAME)
if not category:
logger.info("Creating category '%s'", TEMP_CATEGORY_NAME)
category = await guild.create_category(TEMP_CATEGORY_NAME, reason="Teamify temporary channels")
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om de categorie '{TEMP_CATEGORY_NAME}' te maken.")
move_players = False # Disable moving if category creation fails
logger.error("Missing permissions to create category '%s'", TEMP_CATEGORY_NAME)
except Exception as e:
await ctx.send("Fout bij het maken/vinden van de categorie.")
move_players = False
logger.exception("Error finding/creating category '%s': %s", TEMP_CATEGORY_NAME, e)
# Build message and move players
move_tasks = []
for i, team in enumerate(teams, start=1): for i, team in enumerate(teams, start=1):
team_names = ', '.join([member.mention for member in team]) team_names = ', '.join([member.mention for member in team])
message += f"**Team {i}:** {team_names}\n" message += f"**Team {i}:** {team_names}\n"
if move_players: if move_players and category:
temp_channel = await guild.create_voice_channel(f"Squad {i}", category=category) try:
channel_name = f"Squad {i}"
logger.info("Creating temporary voice channel: %s", channel_name)
temp_channel = await guild.create_voice_channel(channel_name, category=category, reason="Teamify command")
temp_channels.append(temp_channel) temp_channels.append(temp_channel)
for member in team: for member in team:
await member.move_to(temp_channel) # Add move operation to a list to run concurrently
move_tasks.append(member.move_to(temp_channel))
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om voice kanaal 'Squad {i}' te maken of leden te verplaatsen.")
logger.error("Missing permissions to create/move for Squad %d", i)
# Optionally stop creating more channels if one fails
except Exception as e:
await ctx.send(f"Fout bij het maken van kanaal 'Squad {i}'.")
logger.exception("Error creating channel 'Squad %d': %s", i, e)
await text_channel.send(message) # Send the team message
await ctx.send("Teams zijn gepost in #teamify!" + (" Spelers zijn verplaatst naar tijdelijke kanalen." if move_players else "")) try:
await output_text_channel.send(message)
logger.info("Posted team message to #%s", output_text_channel.name)
except discord.Forbidden:
await ctx.send(f"Ik heb geen permissies om berichten te sturen in #{output_text_channel.name}.")
logger.error("Missing permissions to send message in #%s", output_text_channel.name)
except Exception as e:
await ctx.send("Fout bij het versturen van het team bericht.")
logger.exception("Error sending team message: %s", e)
# Execute moves concurrently
if move_tasks:
logger.info("Moving %d members to temporary channels...", len(move_tasks))
results = await asyncio.gather(*move_tasks, return_exceptions=True)
moved_count = 0
for i, result in enumerate(results):
member_to_move = move_tasks[i].__self__ # Get the member object from the coroutine
if isinstance(result, Exception):
logger.error("Failed to move member %s: %s", member_to_move.display_name, result)
try:
# Try to notify in context channel about failed move
await ctx.send(f"Kon {member_to_move.mention} niet verplaatsen: {result}")
except discord.HTTPException: pass # Ignore if sending fails
else:
moved_count += 1
logger.info("Finished moving members. Successful moves: %d", moved_count)
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}! {moved_count} speler(s) verplaatst.")
elif move_players: # If move was intended but failed (e.g., category issue)
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}. Kon spelers niet verplaatsen.")
else:
await ctx.send(f"Teams zijn gepost in #{output_text_channel.name}.")
# Start background task for cleaning up empty channels if channels were created
if temp_channels:
logger.info("Starting background task to clean up %d temporary channels.", len(temp_channels))
bot.loop.create_task(cleanup_empty_channels(guild, temp_channels, ctx.channel))
# Controleer continu of de kanalen leeg zijn en verwijder ze
if move_players:
while temp_channels:
await asyncio.sleep(60) # Controleer elke minuut
for channel in temp_channels[:]:
if len(channel.members) == 0:
if channel in guild.voice_channels: # Controleer of het kanaal nog bestaat
await channel.delete()
temp_channels.remove(channel)
await ctx.send(f"Kanaal {channel.name} is opgeruimd omdat het leeg was!")
@bot.command() @bot.command()
async def whoisbest(ctx, category="Casual", matchesback=18): async def whoisbest(ctx, category="Casual", matchesback: int = 18):
"""
Shows top 3 players by winrate and AHD for a given category.
Usage: !whoisbest [category] [min_matches]
Example: !whoisbest Ranked 10
"""
if category.lower() == "help": if category.lower() == "help":
help_message = ( help_message = (
"**Gebruik van het commando `whoisbest`:**\n" "**Gebruik van `!whoisbest`:**\n"
"`!whoisbest [category] [matchesback]`\n\n" "`!whoisbest [category] [min_matches]`\n\n"
"**Parameters:**\n" "**Parameters:**\n"
"`category` - De categorie van de stats, bijv. 'Casual' of 'Ranked'. Niet hoofdlettergevoelig.\n" "`category` - Stats categorie (bijv. 'Casual', 'Ranked', 'Intense'). Niet hoofdlettergevoelig. Standaard: 'Casual'.\n"
"`matchesback` - Het minimum aantal matches dat een speler gespeeld moet hebben om mee te tellen (standaard 18).\n\n" "`min_matches` - Minimum aantal matches gespeeld. Standaard: 18.\n\n"
"**Voorbeeld:**\n" "**Voorbeeld:** `!whoisbest Ranked 10`\n"
"`!whoisbest Casual 18`\n" "Toont top 3 Win% en AHD voor Ranked met minimaal 10 matches.\n"
"Laat de top 3 spelers zien op basis van winratio en gemiddelde damage in de Casual categorie met minimaal 18 matches.\n\n"
"Typ `!whoisbest help` om deze uitleg opnieuw te zien."
) )
await ctx.send(help_message) await ctx.send(help_message)
return return
logger.info("Whoisbest command invoked: category=%s, matchesback=%d", category, matchesback)
# Bestandspad
file_path = os.path.join("..", "data", "player_last_stats.json")
try: try:
# JSON-bestand lezen # Read JSON file
with open(file_path, "r", encoding="utf-8") as file: if not os.path.exists(STATS_FILE_PATH):
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
logger.error("Stats file not found: %s", STATS_FILE_PATH)
return
with open(STATS_FILE_PATH, "r", encoding="utf-8") as file:
data = json.load(file) data = json.load(file)
# Mapping maken (lowercase -> originele categorie) # Create case-insensitive mapping for categories
category_mapping = {cat.lower(): cat for cat in data.keys()} category_mapping = {cat.lower(): cat for cat in data.keys() if isinstance(data[cat], list)} # Only map list categories
# Zet de opgegeven category om naar lowercase
lower_category = category.lower() lower_category = category.lower()
if lower_category not in category_mapping: if lower_category not in category_mapping:
available_categories = ', '.join(data.keys()) available_categories = ', '.join(category_mapping.values()) # Show original names
await ctx.send(f"Ongeldige categorie '{category}'! Beschikbare categorieën: {available_categories}") await ctx.send(f"Ongeldige categorie '{category}'. Beschikbaar: {available_categories}")
logger.warning("Invalid category requested: %s", category)
return return
# Gebruik de juiste (originele) categorie naam uit de mapping
actual_category = category_mapping[lower_category] actual_category = category_mapping[lower_category]
logger.info("Processing category: %s", actual_category)
players = [player for player in data.get(actual_category, []) if player.get("matches", 0) >= matchesback] # Filter players based on minimum matches
players_in_category = data.get(actual_category, [])
if not players: if not isinstance(players_in_category, list):
await ctx.send(f"Geen spelersstatistieken gevonden voor '{actual_category}' met minimaal {matchesback} gespeelde matches!") await ctx.send(f"Data voor categorie '{actual_category}' is niet in het verwachte formaat.")
logger.error("Invalid data format for category '%s'", actual_category)
return return
# Sorteer spelers op winratio (aflopend) filtered_players = [
top_winratio = sorted(players, key=lambda x: x.get("winratio", 0), reverse=True)[:3] player for player in players_in_category
if isinstance(player, dict) and player.get("matches", 0) >= matchesback
]
logger.info("Found %d players meeting criteria (>=%d matches) in %s", len(filtered_players), matchesback, actual_category)
# Sorteer spelers op gemiddelde damage (aflopend) if not filtered_players:
top_ahd = sorted(players, key=lambda x: x.get("ahd", 0), reverse=True)[:3] await ctx.send(f"Geen spelers gevonden voor '{actual_category}' met ≥ {matchesback} matches.")
return
# Bouw het bericht op # Safely sort players, handling potential missing keys or non-numeric data
message = f"**\U0001F3C6 Top 3 Winratio ({actual_category})**\n" def safe_get_float(player_dict, key, default=0.0):
val = player_dict.get(key)
if isinstance(val, (int, float)):
return float(val)
# Try converting comma decimal separator if needed
if isinstance(val, str):
try: return float(val.replace(',', '.'))
except ValueError: return default
return default
top_winratio = sorted(filtered_players, key=lambda x: safe_get_float(x, "winratio"), reverse=True)[:3]
top_ahd = sorted(filtered_players, key=lambda x: safe_get_float(x, "ahd"), reverse=True)[:3]
# Build the response message
response_message = f"**📊 Top Stats voor '{actual_category}' (min {matchesback} matches)**\n\n"
response_message += "**\U0001F3C6 Top 3 Winratio**\n"
if top_winratio:
for i, player in enumerate(top_winratio, start=1): for i, player in enumerate(top_winratio, start=1):
message += f"{i}. **{player['playername']}** - {player['winratio']:.2f}%\n" player_name = player.get('playername', 'Onbekend')
win_ratio = safe_get_float(player, 'winratio')
response_message += f"{i}. **{player_name}** - {win_ratio:.2f}%\n"
else:
response_message += "_Geen data beschikbaar_\n"
message += f"\n**\U0001F480 Top 3 AHD ({actual_category})**\n" response_message += f"\n**\U0001F4A5 Top 3 Average Human Damage (AHD)**\n" # Changed emoji
if top_ahd:
for i, player in enumerate(top_ahd, start=1): for i, player in enumerate(top_ahd, start=1):
message += f"{i}. **{player['playername']}** - {player['ahd']:.2f}\n" player_name = player.get('playername', 'Onbekend')
ahd_val = safe_get_float(player, 'ahd')
response_message += f"{i}. **{player_name}** - {ahd_val:.2f}\n"
else:
response_message += "_Geen data beschikbaar_\n"
await ctx.send(message) await ctx.send(response_message)
logger.info("Successfully sent whoisbest stats for %s", actual_category)
except FileNotFoundError:
await ctx.send(f"Statistieken bestand niet gevonden ({STATS_FILE_PATH}).")
logger.error("Stats file not found: %s", STATS_FILE_PATH)
except json.JSONDecodeError:
await ctx.send("Fout bij het lezen van de statistieken (ongeldig JSON).")
logger.exception("JSONDecodeError reading stats file: %s", STATS_FILE_PATH)
except Exception as e: except Exception as e:
await ctx.send(f"Fout bij het laden van de statistieken: {str(e)}") await ctx.send(f"Onverwachte fout bij het ophalen van statistieken: {e}")
logger.exception("Error in whoisbest command: %s", e)
@bot.event
async def on_voice_state_update(member, before, after):
logging_channel = discord.utils.get(member.guild.text_channels, name="logging")
if not logging_channel:
return
if (before.channel and before.channel.name == "GOD_CHANNEL") or (after.channel and after.channel.name == "GOD_CHANNEL"):
return # Als het God_channel is, doe niks
if before.channel is None and after.channel is not None:
# Lid joint een voice channel
await logging_channel.send(f"🔊 {member.mention} is gejoined in voice kanaal: **{after.channel.name}**")
elif before.channel is not None and after.channel is None:
# Lid verlaat een voice channel
await logging_channel.send(f"🔇 {member.mention} heeft het voice kanaal **{before.channel.name}** verlaten.")
elif before.channel != after.channel:
# Lid switched van voice kanaal
await logging_channel.send(f"🔄 {member.mention} is van **{before.channel.name}** naar **{after.channel.name}** gegaan.")
@bot.event
async def on_member_join(member):
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
if logging_channel:
await logging_channel.send(f"🎉 Welkom {member.mention} op de server! We hopen dat je een leuke tijd hebt!")
@bot.event
async def on_member_remove(member):
logging_channel = discord.utils.get(member.guild.text_channels, name="raadhuisplein")
if logging_channel:
await logging_channel.send(f"😢 {member.name} heeft de server verlaten. We zullen je missen!")
@bot.command() @bot.command()
@commands.has_permissions(move_members=True) # Add permission check
async def moveall(ctx): async def moveall(ctx):
# Controleer of het commando in het juiste kanaal wordt uitgevoerd """Moves all members from other voice channels to the 'teamify' channel."""
if ctx.channel.name != "teamify": # Check if command is used in the correct text channel
await ctx.send("Dit commando kan alleen worden gebruikt in het #teamify tekstkanaal.") if ctx.channel.name != TEAMIFY_TEXT_CHANNEL:
await ctx.send(f"Dit commando kan alleen worden gebruikt in het #{TEAMIFY_TEXT_CHANNEL} tekstkanaal.")
return return
guild = ctx.guild guild = ctx.guild
teamify_channel = discord.utils.get(guild.voice_channels, name="teamify") target_channel = discord.utils.get(guild.voice_channels, name=TEAMIFY_VOICE_CHANNEL)
if not teamify_channel: if not target_channel:
await ctx.send("Het teamify voice-kanaal bestaat niet!") await ctx.send(f"Het '{TEAMIFY_VOICE_CHANNEL}' voice-kanaal bestaat niet!")
logger.error("Target voice channel '%s' not found for moveall.", TEAMIFY_VOICE_CHANNEL)
return return
moved_members = 0 moved_members_count = 0
move_tasks = []
members_to_move = []
logger.info("Moveall command initiated by %s", ctx.author.display_name)
for channel in guild.voice_channels: for channel in guild.voice_channels:
if channel != teamify_channel: # Skip the target channel itself and channels with no members
if channel == target_channel or not channel.members:
continue
for member in channel.members: for member in channel.members:
# Avoid moving bots unless intended
# if member.bot: continue
members_to_move.append(member)
move_tasks.append(member.move_to(target_channel))
if not move_tasks:
await ctx.send("Er waren geen spelers in andere kanalen om te verplaatsen.")
logger.info("Moveall: No members found in other channels.")
return
logger.info("Attempting to move %d members to #%s", len(move_tasks), target_channel.name)
results = await asyncio.gather(*move_tasks, return_exceptions=True)
for i, result in enumerate(results):
member = members_to_move[i]
if isinstance(result, Exception):
logger.error("Failed to move member %s: %s", member.display_name, result)
try: try:
await member.move_to(teamify_channel) await ctx.send(f"Kon {member.mention} niet verplaatsen: {result}")
moved_members += 1 except discord.HTTPException: pass
except Exception as e:
await ctx.send(f"Kon {member.mention} niet verplaatsen: {e}")
if moved_members > 0:
await ctx.send(f"{moved_members} speler(s) zijn verplaatst naar het teamify kanaal.")
else: else:
await ctx.send("Er waren geen spelers om te verplaatsen.") moved_members_count += 1
bot.run(token) logger.info("Moveall finished. Successfully moved %d members.", moved_members_count)
if moved_members_count > 0:
await ctx.send(f"{moved_members_count} speler(s) zijn verplaatst naar het {target_channel.mention} kanaal.")
else:
await ctx.send("Kon geen spelers verplaatsen (controleer permissies?).")
@moveall.error
async def moveall_error(ctx, error):
"""Error handler specific to moveall command."""
if isinstance(error, commands.MissingPermissions):
await ctx.send("Je hebt geen permissies om leden te verplaatsen.")
else:
# Log and notify for other errors related to moveall
logger.exception("Error in moveall command: %s", error)
await ctx.send("Er is een fout opgetreden bij het uitvoeren van `moveall`.")
# --- Run Bot ---
if __name__ == "__main__":
logger.info("Starting bot...")
bot.run(token)

View file

@ -1,52 +1,80 @@
function new-lock { # Functions for creating and removing a simple lock file to prevent concurrent script execution.
function New-Lock {
[CmdletBinding()] [CmdletBinding()]
param ( param (
[Parameter()] [Parameter()]
[string] [string]$by # Optional identifier for who/what created the lock
$by
) )
Write-Output 'Setting lock' Write-Verbose "Attempting to acquire lock..."
$timeout = 15 $timeoutSeconds = 15 * 15 # Increased timeout to ~4 minutes (15 attempts * 15 seconds)
$i = 0 $sleepSeconds = 15
$startTime = Get-Date
# Determine lock file path based on OS
if ($IsWindows) {
$lockFilePath = Join-Path -Path $env:TEMP -ChildPath 'pubg_stats.lock'
} elseif ($IsLinux -or $IsMacOS) {
$lockFilePath = '/tmp/pubg_stats.lock'
} else {
Write-Error "Unsupported operating system for lock file path determination."
throw "Unsupported OS for locking." # Throw to ensure script stops
}
Write-Verbose "Using lock file path: $lockFilePath"
while ($true) { while ($true) {
if ($env:temp) { if (Test-Path -Path $lockFilePath -PathType Leaf) {
$lockFile = Join-Path -Path $env:temp -ChildPath 'lockfile_pubg.lock' $lockContent = Get-Content -Path $lockFilePath -Raw -ErrorAction SilentlyContinue
} Write-Warning ("Lock file '$lockFilePath' already exists (Content: '$lockContent'). Waiting $sleepSeconds seconds...")
else { Start-Sleep -Seconds $sleepSeconds
$lockFile = "/tmp/lockfile_pubg.lock" } else {
}
if (Test-Path -Path $lockFile) {
Write-Host "Job is already running. Lock file found at $lockFile. Sleeping 15 seconds."
Start-Sleep -Seconds 15
}
else {
try { try {
$content = if ($by) { $by } else { "" } # Attempt to create the lock file
New-Item -ItemType File -Path $lockFile -Value $content -Force $lockCreatorInfo = if ($by) { "Locked by '$by' at $(Get-Date)" } else { "Locked at $(Get-Date)" }
Write-Host "Lock file created at $lockFile." New-Item -ItemType File -Path $lockFilePath -Value $lockCreatorInfo -Force -ErrorAction Stop | Out-Null
break Write-Output "Lock file created successfully at '$lockFilePath'."
} return # Exit the loop and function on success
catch { } catch {
Write-Output "Unable to create lockfile, error: $_. Resuming lock loop." # This catch might occur if another process creates the file between Test-Path and New-Item (race condition)
$errorMessage = $_.Exception.Message
Write-Warning "Failed to create lock file (possible race condition?): $errorMessage. Retrying..."
Start-Sleep -Seconds 1 # Short sleep before retry on creation failure
} }
} }
if ($i -ge $timeout) {
Write-Output "Timed out after $timeout attempts." # Check for timeout
exit if (((Get-Date) - $startTime).TotalSeconds -ge $timeoutSeconds) {
Write-Error "Timed out after $timeoutSeconds seconds waiting for lock file '$lockFilePath'."
throw "Lock acquisition timed out." # Throw to ensure script stops
} }
$i++
} }
} }
function remove-lock { function Remove-Lock {
Write-Output 'Removing lock' Write-Verbose "Attempting to remove lock..."
if ($env:temp) {
$lockFile = Join-Path -Path $env:temp -ChildPath 'lockfile_pubg.lock' # Determine lock file path (consistent with New-Lock)
if ($IsWindows) {
$lockFilePath = Join-Path -Path $env:TEMP -ChildPath 'pubg_stats.lock'
} elseif ($IsLinux -or $IsMacOS) {
$lockFilePath = '/tmp/pubg_stats.lock'
} else {
Write-Warning "Unsupported operating system for lock file path determination. Cannot remove lock."
return
} }
else { Write-Verbose "Target lock file path: $lockFilePath"
$lockFile = "/tmp/lockfile_pubg.lock"
if (Test-Path -Path $lockFilePath -PathType Leaf) {
try {
Remove-Item -Path $lockFilePath -Force -ErrorAction Stop
Write-Output "Lock file '$lockFilePath' removed successfully."
} catch {
$errorMessage = $_.Exception.Message
Write-Error "Failed to remove lock file '$lockFilePath'. Manual removal might be required. Error: $errorMessage"
# Consider if this should be a fatal error depending on script requirements
}
} else {
Write-Warning "Lock file '$lockFilePath' not found. Assuming already removed."
} }
Remove-Item -Path $lockFile
} }

View file

@ -22,20 +22,46 @@ foreach ($_SESSION['access_times'] as $key => $timestamp) {
if (count($_SESSION['access_times']) > $allowed_refreshes) { if (count($_SESSION['access_times']) > $allowed_refreshes) {
die(' // Set HTTP status code for rate limiting
<!DOCTYPE html> http_response_code(429); // Too Many Requests
<html lang="en"> ?>
<!DOCTYPE html>
<html lang="nl">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="refresh" content="3"> <meta http-equiv="refresh" content="5;url=<?php echo htmlspecialchars($_SERVER['REQUEST_URI']); ?>">
<title>RustAGHH</title> <title>Te Veel Verzoeken</title>
<style>
body { font-family: sans-serif; padding: 20px; background-color: #f8f8f8; color: #333; }
.container { max-width: 600px; margin: 50px auto; background-color: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
h1 { color: #d9534f; }
p { line-height: 1.6; }
.timer { font-weight: bold; }
</style>
</head> </head>
<body> <body>
<p>RUSTAAAGGHHHH je mag de pagina niet vaker dan ' . $allowed_refreshes . 'x per ' . $allowed_time . ' seconde refreshen. Over een paar seconden wordt je weer teruggeleid.</p> <div class="container">
<h1>Rustâââgh!</h1>
<p>Je probeert de pagina te vaak te vernieuwen (meer dan <?php echo $allowed_refreshes; ?> keer per <?php echo $allowed_time; ?> seconden).</p>
<p>Wacht even, je wordt over <span id="countdown" class="timer">5</span> seconden automatisch teruggestuurd.</p>
</div>
<script>
let seconds = 5;
const countdownElement = document.getElementById('countdown');
const interval = setInterval(() => {
seconds--;
countdownElement.textContent = seconds;
if (seconds <= 0) {
clearInterval(interval);
// Optional: You could redirect here as well, but meta refresh handles it
// window.location.href = "<?php echo htmlspecialchars($_SERVER['REQUEST_URI']); ?>";
}
}, 1000);
</script>
</body> </body>
</html> </html>
'); <?php
exit; // Use exit instead of die for clarity
} }
?> ?>

256
index.php
View file

@ -1,32 +1,86 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Welcome to the epicenter of PUBG action! Catch up on the latest matches with detailed stats including player names, match dates, modes, types, maps, kills, and more. Plus, get an inside look at our clan's profile, including member details and key attributes. Stay connected with the pulse of our PUBG community!"; $ogDescription = "Welcome to the epicenter of PUBG action! Catch up on the latest matches with detailed stats including player names, match dates, modes, types, maps, kills, and more. Plus, get an inside look at our clan's profile, including member details and key attributes. Stay connected with the pulse of our PUBG community!";
// Read the JSON file // Include map names mapping
$jsonData = file_get_contents('data/player_matches.json'); include './includes/mapsmap.php'; // Contains the $mapNames array
$playersData = json_decode($jsonData, true);
// Combine matches from all players // --- Latest Matches Data ---
$allMatches = []; $matchesJsonPath = 'data/player_matches.json';
foreach ($playersData as $player) { $lastMatches = [];
$matchesError = '';
if (file_exists($matchesJsonPath)) {
$jsonData = file_get_contents($matchesJsonPath);
$playersData = json_decode($jsonData, true);
if (is_array($playersData)) {
// Combine matches from all players
$allMatches = [];
foreach ($playersData as $player) {
if (isset($player['player_matches']) && is_array($player['player_matches'])) {
foreach ($player['player_matches'] as $match) { foreach ($player['player_matches'] as $match) {
$match['playername'] = $player['playername']; // Add playername to each match for reference $match['playername'] = $player['playername'] ?? 'Unknown'; // Add playername to each match for reference
$allMatches[] = $match; $allMatches[] = $match;
} }
}
}
// Sort matches by createdAt date (descending)
usort($allMatches, function ($a, $b) {
$timeA = isset($a['createdAt']) ? strtotime($a['createdAt']) : 0;
$timeB = isset($b['createdAt']) ? strtotime($b['createdAt']) : 0;
return $timeB - $timeA;
});
// Get the last 8 matches
$lastMatches = array_slice($allMatches, 0, 8);
} else {
$matchesError = "Error decoding player matches data.";
}
} else {
$matchesError = "Player matches data file not found.";
} }
// Sort matches by createdAt date // --- Clan Info Data ---
usort($allMatches, function ($a, $b) { $clanInfoPath = './data/claninfo.json';
return strtotime($b['createdAt']) - strtotime($a['createdAt']); $clanmembersfile = './config/clanmembers.json';
}); $rankedfile = './data/player_season_data.json';
// Get the last 5 matches $clan = null;
$lastMatches = array_slice($allMatches, 0, 8); $playerRanks = null;
$clanInfoError = '';
if (file_exists($clanInfoPath)) {
$clanJson = file_get_contents($clanInfoPath);
$clan = json_decode($clanJson, true);
if (!is_array($clan)) {
$clanInfoError = "Error decoding clan info data.";
$clan = null;
}
} else {
$clanInfoError = "Clan info file missing.";
}
if (file_exists($rankedfile)) {
$rankedJson = file_get_contents($rankedfile);
$playerRanks = json_decode($rankedJson, true);
if (!is_array($playerRanks)) {
$clanInfoError .= " Error decoding player rank data.";
$playerRanks = null;
}
} else {
$clanInfoError .= " Player rank file missing.";
}
// Note: $clanmembers is read but not used directly in this refactored version's display logic.
// If needed elsewhere, ensure file_exists check is added.
// $clanmembers = file_exists($clanmembersfile) ? json_decode(file_get_contents($clanmembersfile), true) : null;
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
@ -36,9 +90,13 @@ $lastMatches = array_slice($allMatches, 0, 8);
<main> <main>
<section> <section>
<h2>Latest Matches</h2> <h2>Latest Matches</h2>
<?php if ($matchesError): ?>
<p style="color: red;"><?php echo htmlspecialchars($matchesError); ?></p>
<?php elseif (empty($lastMatches)): ?>
<p>No recent matches found.</p>
<?php else: ?>
<table> <table>
<thead>
<tr> <tr>
<th>Player Name</th> <th>Player Name</th>
<th>Match Date</th> <th>Match Date</th>
@ -49,87 +107,107 @@ $lastMatches = array_slice($allMatches, 0, 8);
<th>Damage</th> <th>Damage</th>
<th>Place</th> <th>Place</th>
</tr> </tr>
</thead>
<tbody>
<?php <?php foreach ($lastMatches as $match):
include './includes/mapsmap.php'; $matchid = htmlspecialchars($match['id'] ?? '');
$playerName = htmlspecialchars($match['playername'] ?? 'N/A');
$gameMode = htmlspecialchars($match['gameMode'] ?? 'N/A');
foreach ($lastMatches as $match) { $matchType = htmlspecialchars($match['matchType'] ?? 'N/A');
$matchid = $match['id']; $mapNameRaw = $match['mapName'] ?? 'N/A';
$date = new DateTime($match['createdAt']); $mapName = htmlspecialchars(isset($mapNames[$mapNameRaw]) ? $mapNames[$mapNameRaw] : $mapNameRaw);
$date->modify('+1 hours'); $kills = htmlspecialchars($match['stats']['kills'] ?? 'N/A');
$damageDealt = isset($match['stats']['damageDealt']) ? number_format($match['stats']['damageDealt'], 0, '.', '') : 'N/A';
$winPlace = htmlspecialchars($match['stats']['winPlace'] ?? 'N/A');
$createdAt = $match['createdAt'] ?? null;
$formattedDate = 'N/A';
if ($createdAt) {
try {
$date = new DateTime($createdAt);
$date->modify('+1 hours'); // Adjust timezone or add offset as needed
$formattedDate = $date->format('m-d H:i:s'); $formattedDate = $date->format('m-d H:i:s');
echo "<tr> } catch (Exception $e) {
$formattedDate = 'Invalid Date';
<td><a href='matchinfo.php?matchid=$matchid'>" . $match['playername'] . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . $formattedDate . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . $match['gameMode'] . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . $match['matchType'] . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . (isset($mapNames[$match['mapName']]) ? $mapNames[$match['mapName']] : $match['mapName']) . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . $match['stats']['kills'] . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . number_format($match['stats']['damageDealt'], 0, '.', '') . "</a></td>
<td><a href='matchinfo.php?matchid=$matchid'>" . $match['stats']['winPlace'] . "</a></td>
</tr>";
} ?>
</table>
<h2>Clan Info</h2>
<?php
//CLANINFO
$clanInfoPath = './data/claninfo.json';
$clanmembersfile = './config/clanmembers.json';
$rankedfile = './data/player_season_data.json';
$clanmembers = json_decode(file_get_contents($clanmembersfile), true);
$playerRanks = json_decode(file_get_contents($rankedfile), true);
if (file_exists($clanInfoPath)) {
$clan = json_decode(file_get_contents($clanInfoPath), true);
if (isset($clan) && !empty($clan)) {
echo "<table class='sortable'>";
echo "<tr><th>Attribute</th><th>Value</th><th>Rank(FPP SQUAD)</th><th>Points</th></tr>";
foreach ($playerRanks as $rank) {
$playername = $rank['name'];
if (isset($rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp'])) {
$tier = $rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp']['currentTier']['tier'];
$subTier = $rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp']['currentTier']['subTier'];
$image = "./images/ranks/" . $tier . "-" . $subTier . ".webp";
$rankPoint = htmlspecialchars($rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp']['currentRankPoint']);
echo "<tr><td><a href='latestmatches.php?selected_player=" . htmlspecialchars($playername) . "'>name</a></td><td><a href='latestmatches.php?selected_player=" . htmlspecialchars($playername) . "'>" . htmlspecialchars($playername) . "</a></td><td><img src='" . $image . "' class='table-image' alt='$tier'></td><td>" . $rankPoint . "</td></tr>";
} else {
echo "<tr><td><a href='latestmatches.php?selected_player=" . htmlspecialchars($playername) . "'>name</a></td><td><a href='latestmatches.php?selected_player=" . htmlspecialchars($playername) . "'>" . htmlspecialchars($playername) . "</a></td><td><img src='./images/ranks/Unranked.webp' class='table-image' alt='$tier'></td><td></td></tr>";
} }
}
foreach ($clan as $key => $value) {
if ($key == 'updated') {
continue;
}
echo "<tr><td>" . htmlspecialchars($key) . "</td><td>" . htmlspecialchars($value) . "</td><td></td><td></td></tr>";
}
echo "</table>";
} else {
echo "<p>No clan attributes available</p>";
}
} else {
echo "<p>Clan info file missing</p>";
} }
?> ?>
<tr>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $playerName; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $formattedDate; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $gameMode; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $matchType; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $mapName; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $kills; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $damageDealt; ?></a></td>
<td><a href="matchinfo.php?matchid=<?php echo $matchid; ?>"><?php echo $winPlace; ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<section>
<h2>Clan Info</h2>
<?php if ($clanInfoError && !$clan && !$playerRanks): ?>
<p style="color: red;"><?php echo htmlspecialchars(trim($clanInfoError)); ?></p>
<?php else: ?>
<?php if ($clanInfoError): // Show non-fatal errors ?>
<p style="color: orange;"><?php echo htmlspecialchars(trim($clanInfoError)); ?></p>
<?php endif; ?>
<?php if (isset($clan) && !empty($clan)): ?>
<table class="sortable">
<thead>
<tr><th>Attribute</th><th>Value</th><th>Rank (FPP SQUAD)</th><th>Points</th></tr>
</thead>
<tbody>
<?php if (isset($playerRanks) && is_array($playerRanks)): ?>
<?php foreach ($playerRanks as $rank):
$playername = htmlspecialchars($rank['name'] ?? 'N/A');
$playerLink = 'latestmatches.php?selected_player=' . urlencode($rank['name'] ?? '');
$tier = 'Unranked';
$subTier = '';
$image = './images/ranks/Unranked.webp';
$rankPoint = '';
if (isset($rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp'])) {
$squadFppStats = $rank['stat']['data']['attributes']['rankedGameModeStats']['squad-fpp'];
$tier = htmlspecialchars($squadFppStats['currentTier']['tier'] ?? 'N/A');
$subTier = htmlspecialchars($squadFppStats['currentTier']['subTier'] ?? '');
$image = "./images/ranks/" . $tier . "-" . $subTier . ".webp";
$rankPoint = htmlspecialchars($squadFppStats['currentRankPoint'] ?? '');
}
$altText = $tier . ($subTier ? '-' . $subTier : '');
?>
<tr>
<td><a href="<?php echo $playerLink; ?>">name</a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $playername; ?></a></td>
<td><img src="<?php echo htmlspecialchars($image); ?>" class="table-image" alt="<?php echo htmlspecialchars($altText); ?>"></td>
<td><?php echo $rankPoint; ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
<?php foreach ($clan as $key => $value):
if ($key == 'updated') continue; // Skip updated timestamp
?>
<tr>
<td><?php echo htmlspecialchars($key); ?></td>
<td><?php echo htmlspecialchars(is_scalar($value) ? $value : json_encode($value)); ?></td>
<td></td>
<td></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php elseif (!$clanInfoError): // Only show if no error message was already displayed ?>
<p>No clan attributes available.</p>
<?php endif; ?>
<?php endif; ?>
</section> </section>
</main> </main>
<?php include './includes/footer.php'; ?> <?php include './includes/footer.php'; ?>
</body> </body>
</html> </html>

View file

@ -1,11 +1,119 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Explore detailed player statistics over the past month including win percentages, K/D ratios in human and all-player categories, total kills, and more. Delve into stats for various match types like Intense, Casual, Official, Custom, and Ranked, and see how players have fared in a minimum number of matches. Stay informed with the latest updates on player performance."; $ogDescription = "Explore detailed player statistics over the past month including win percentages, K/D ratios in human and all-player categories, total kills, and more. Delve into stats for various match types like Intense, Casual, Official, Custom, and Ranked, and see how players have fared in a minimum number of matches. Stay informed with the latest updates on player performance.";
?>
// Include config if it exists
$configPath = './config/config.php';
if (file_exists($configPath)) {
include $configPath;
}
$clanmembersData = null;
$alts = [];
$playersStatsData = null;
$processedStats = [];
$dataError = '';
$lastUpdated = 'N/A';
// Category definitions with minimum match requirements and display names
$categories = [
// 'all' => ['min_matches' => 25, 'display_name' => 'All Matches (min 25)'], // Skipping 'all' as per original logic
'clan_casual' => ['min_matches' => 18, 'display_name' => 'Clan Casual (min 18 matches, min 2 clan players)'],
'Intense' => ['min_matches' => 18, 'display_name' => 'Intense BR (min 18 matches)'],
'Casual' => ['min_matches' => 18, 'display_name' => 'Casual (min 18 matches)'],
'official' => ['min_matches' => 18, 'display_name' => 'Official (min 18 matches)'],
'custom' => ['min_matches' => 8, 'display_name' => 'Custom (min 8 matches)'],
'Ranked' => ['min_matches' => 8, 'display_name' => 'Ranked (min 8 matches)'],
];
// Load clan members to identify alts
$clanMembersPath = './config/clanmembers.json';
if (file_exists($clanMembersPath)) {
$clanmembersJson = file_get_contents($clanMembersPath);
$clanmembersData = json_decode($clanmembersJson, true);
if (isset($clanmembersData['alts']) && is_array($clanmembersData['alts'])) {
$alts = $clanmembersData['alts'];
} else {
$dataError .= " Error reading or invalid format for 'alts' in clan members file.";
}
} else {
$dataError .= " Clan members file not found.";
}
// Load player stats data
$statsPath = './data/player_last_stats.json';
if (file_exists($statsPath)) {
$statsJson = file_get_contents($statsPath);
$playersStatsData = json_decode($statsJson, true);
if (!is_array($playersStatsData)) {
$dataError .= " Error decoding player last stats data.";
$playersStatsData = null;
} else {
$lastUpdated = htmlspecialchars($playersStatsData['updated'] ?? 'N/A');
// Process stats for each defined category
foreach ($categories as $key => $categoryInfo) {
if (isset($playersStatsData[$key]) && is_array($playersStatsData[$key])) {
$categoryStats = [];
foreach ($playersStatsData[$key] as $player_data) {
// Basic validation and alt check
if (!isset($player_data['playername']) || is_null($player_data['playername']) || in_array($player_data['playername'], $alts)) {
continue;
}
// Minimum matches check
$matchesPlayed = $player_data['matches'] ?? 0;
if ($matchesPlayed < $categoryInfo['min_matches']) {
continue;
}
// Format stats for display
$formatted_player_data = [];
$formatted_player_data['playername'] = htmlspecialchars($player_data['playername']);
$formatted_player_data['deaths'] = number_format($player_data['deaths'] ?? 0, 0, ',', '');
$formatted_player_data['kills'] = number_format($player_data['kills'] ?? 0, 0, ',', '');
$formatted_player_data['humankills'] = number_format($player_data['humankills'] ?? 0, 0, ',', '');
$formatted_player_data['matches'] = htmlspecialchars($matchesPlayed);
$formatted_player_data['wins'] = number_format($player_data['wins'] ?? 0, 0, ',', '');
$formatted_player_data['winratio'] = number_format($player_data['winratio'] ?? 0, 2, ',', '');
$formatted_player_data['ahd'] = number_format($player_data['ahd'] ?? 0, 2, ',', '');
// Format K/D (handle null, Infinity, non-numeric)
$kd_h_raw = $player_data['KD_H'] ?? null;
$formatted_player_data['KD_H'] = ($kd_h_raw === null) ? "N/A" : (($kd_h_raw == "Infinity") ? "" : (is_numeric($kd_h_raw) ? number_format((float)$kd_h_raw, 2, ',', '') : "0"));
$kd_all_raw = $player_data['KD_ALL'] ?? null;
$formatted_player_data['KD_ALL'] = ($kd_all_raw === null) ? "N/A" : (($kd_all_raw == "Infinity") ? "" : (is_numeric($kd_all_raw) ? number_format((float)$kd_all_raw, 2, ',', '') : "0"));
// Format change indicator
$originalChange = isset($player_data['change']) ? str_replace(',', '.', $player_data['change']) : '0';
$changeValue = floatval($originalChange);
$formatted_player_data['change_value'] = number_format($changeValue, 2, ',', ''); // Display formatted number
if ($changeValue < 0) {
$formatted_player_data['change_image'] = 'images/red.png';
$formatted_player_data['change_alt'] = 'Decrease';
} elseif ($changeValue > 0) {
$formatted_player_data['change_image'] = 'images/green.png';
$formatted_player_data['change_alt'] = 'Increase';
} else {
$formatted_player_data['change_image'] = 'images/equal.png';
$formatted_player_data['change_alt'] = 'No change';
}
$categoryStats[] = $formatted_player_data;
}
$processedStats[$key] = $categoryStats;
}
}
}
} else {
$dataError .= " Player last stats data file not found.";
}
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
@ -15,45 +123,22 @@ $ogDescription = "Explore detailed player statistics over the past month includi
<main> <main>
<section> <section>
<h2>Player Stats past Quarter</h2> <h2>Player Stats Past Quarter</h2>
<?php
include './config/config.php';
$clanmembers = json_decode(file_get_contents('./config/clanmembers.json'), true);
$alts = $clanmembers['alts'];
$players_matches = json_decode(file_get_contents('./data/player_last_stats.json'), true);
foreach ($players_matches as $key => $player_datas) { <?php if (trim($dataError)): ?>
if ($key == 'updated') { <p style="color: red;"><?php echo htmlspecialchars(trim($dataError)); ?></p>
continue; <?php endif; ?>
}
if ($key == 'all') {
continue;
}
echo "<br>"; <?php foreach ($categories as $key => $categoryInfo): ?>
// if ($key == 'all') { <?php if (isset($processedStats[$key])): ?>
// echo "Stats for $key (minimal 25 matches)"; <br>
// } <h3><?php echo htmlspecialchars($categoryInfo['display_name']); ?></h3>
if ($key == 'clan_casual') { <?php if (empty($processedStats[$key])): ?>
echo "Stats for $key (minimal 18 matches) - Clan casual min 2 clan players per match"; <p>No players met the criteria for this category.</p>
} <?php else: ?>
if ($key == 'Intense') { <table border="1" class="sortable">
echo "Stats for $key (minimal 18 matches)"; <thead>
} <tr>
if ($key == 'Casual') {
echo "Stats for $key (minimal 18 matches)";
}
if ($key == 'official') {
echo "Stats for $key (minimal 18 matches)";
}
if ($key == 'custom') {
echo "Stats for $key (minimal 8 matches)";
}
if ($key == 'Ranked') {
echo "Stats for $key (minimal 8 matches)";
}
echo "<table border='1' class='sortable'>";
echo "<tr>
<th>Player</th> <th>Player</th>
<th>Win %</th> <th>Win %</th>
<th>AHD</th> <th>AHD</th>
@ -61,119 +146,45 @@ $ogDescription = "Explore detailed player statistics over the past month includi
<th>Human Kills</th> <th>Human Kills</th>
<th>K/D All</th> <th>K/D All</th>
<th>Kills</th> <th>Kills</th>
<th>Mtchs</th> <th>Matches</th>
<th>Wins</th> <th>Wins</th>
<th>Deaths</th> <th>Deaths</th>
<th>Win % change</th> <th>Win % Change</th>
</tr>
</thead>
</tr>"; <tbody>
foreach ($player_datas as $player_data) { <?php foreach ($processedStats[$key] as $player_stat):
if (!isset($player_data['playername']) || is_null($player_data['playername'])) { $playerLink = 'latestmatches.php?selected_player=' . urlencode($player_stat['playername']);
continue; // Skip this iteration and move to the next
}
if (in_array($player_data['playername'], $alts)) {
continue; // Skip alt players
}
if ($key == 'all' && $player_data['matches'] < 25) {
continue;
}
if ($key == 'clan_casual' && $player_data['matches'] < 18) {
continue;
}
if ($key == 'Intense' && $player_data['matches'] < 18) {
continue;
}
if ($key == 'Casual' && $player_data['matches'] < 18) {
continue;
}
if ($key == 'official' && $player_data['matches'] < 18) {
continue;
}
if ($key == 'custom' && $player_data['matches'] < 8) {
continue;
}
if ($key == 'Ranked' && $player_data['matches'] < 8) {
continue;
}
$player_name = $player_data['playername'];
$deaths = number_format($player_data['deaths'], 0, ',', '');
$kills = number_format($player_data['kills'], 0, ',', '');
$humankills = number_format($player_data['humankills'], 0, ',', '');
$matches = $player_data['matches'];
$KD_H =
!isset($player_data['KD_H']) || $player_data['KD_H'] === null
? "null"
: ($player_data['KD_H'] == "Infinity"
? ""
: (is_numeric($player_data['KD_H'])
? number_format((float) $player_data['KD_H'], 2, ',', '')
: "0")); // or any other default string for non-numerical values
$KD_ALL =
!isset($player_data['KD_ALL']) || $player_data['KD_ALL'] === null
? "null"
: ($player_data['KD_ALL'] == "Infinity"
? ""
: (is_numeric($player_data['KD_ALL'])
? number_format((float) $player_data['KD_ALL'], 2, ',', '')
: "0")); // or any other default string for non-numerical values
$wins = number_format($player_data['wins'], 0, ',', '');
$winratio = number_format($player_data['winratio'], 2, ',', '');
$originalChange = str_replace(',', '.', $player_data['change']); // replace comma with period
$change = floatval($originalChange);
$ahd = number_format($player_data['ahd'], 2, ',', '');
if ($originalChange < 0) {
$imagePath = 'images\red.png';
} elseif ($change > 0) {
$imagePath = 'images\green.png';
} else {
$imagePath = 'images\equal.png';
}
echo "<tr>
<td><a href='latestmatches.php?selected_player=$player_name'>$player_name</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$winratio</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$ahd</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$KD_H</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$humankills</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$KD_ALL</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$kills</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$matches</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$wins</a></td>
<td><a href='latestmatches.php?selected_player=$player_name'>$deaths</a></td>
<td style='line-height: 17px;'><img src='$imagePath' alt='Change Indicator' style='vertical-align: middle;' width='17' height='17'/> $change </td>
</tr>";
}
echo "</table>";
}
foreach ($players_matches as $key => $update) {
if ($key == 'updated') {
echo "Last update: $update ";
}
}
?> ?>
<tr>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['playername']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['winratio']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['ahd']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['KD_H']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['humankills']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['KD_ALL']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['kills']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['matches']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['wins']; ?></a></td>
<td><a href="<?php echo $playerLink; ?>"><?php echo $player_stat['deaths']; ?></a></td>
<td style="line-height: 17px;">
<img src="<?php echo htmlspecialchars($player_stat['change_image']); ?>" alt="<?php echo htmlspecialchars($player_stat['change_alt']); ?>" style="vertical-align: middle;" width="17" height="17"/>
<?php echo $player_stat['change_value']; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php endif; ?>
<?php endforeach; ?>
<p>Last update: <?php echo $lastUpdated; ?></p>
</section> </section>
</main> </main>
<?php include './includes/footer.php'; ?> <?php include './includes/footer.php'; ?>
</body> </body>
</html> </html>

View file

@ -1,12 +1,113 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Dive into the detailed match stats of DTCH Clan in PUBG. Explore recent matches, various game modes, and match types. View individual performance metrics like kills, damage dealt, and survival time for each clan member. Stay updated with the latest match stats and follow the clan's journey in PUBG."; $ogDescription = "Dive into the detailed match stats of DTCH Clan in PUBG. Explore recent matches, various game modes, and match types. View individual performance metrics like kills, damage dealt, and survival time for each clan member. Stay updated with the latest match stats and follow the clan's journey in PUBG.";
// Include required files
include './includes/mapsmap.php'; // Contains the $mapNames array
$configPath = './config/config.php';
if (file_exists($configPath)) {
include $configPath;
}
$players_matches = null;
$clanMembers = [];
$filteredMatches = [];
$dataError = '';
$selected_player = null;
$filter_by_match_type = 'all'; // Default filter
$matchTypes = ['all', 'airoyale', 'official', 'custom', 'event', 'competitive']; // Available filter types
// Load clan members
$clanMembersPath = './config/clanmembers.json';
if (file_exists($clanMembersPath)) {
$playersJson = file_get_contents($clanMembersPath);
$playersData = json_decode($playersJson, true);
if (isset($playersData['clanMembers']) && is_array($playersData['clanMembers'])) {
$clanMembers = $playersData['clanMembers'];
} else {
$dataError .= " Error reading or invalid format in clan members file.";
}
} else {
$dataError .= " Clan members file not found.";
}
// Determine selected player
if (!empty($clanMembers)) {
$selected_player_from_get = $_GET['selected_player'] ?? null;
if ($selected_player_from_get && in_array($selected_player_from_get, $clanMembers)) {
$selected_player = $selected_player_from_get;
} else {
$selected_player = $clanMembers[0]; // Default to the first clan member
}
} else {
$dataError .= " No clan members loaded to select from.";
}
// Determine selected match type filter
$filter_from_get = $_GET['filter_by_match_type'] ?? 'all';
if (in_array($filter_from_get, $matchTypes)) {
$filter_by_match_type = $filter_from_get;
}
// Load cached matches data
$matchesPath = './data/cached_matches.json';
if (file_exists($matchesPath)) {
$matchesJson = file_get_contents($matchesPath);
$players_matches = json_decode($matchesJson, true);
if (!is_array($players_matches)) {
$dataError .= " Error decoding cached matches data.";
$players_matches = null;
} elseif ($selected_player) {
// Filter matches for the selected player and match type
foreach ($players_matches as $match) {
if (!isset($match['stats']) || !is_array($match['stats'])) continue;
foreach ($match['stats'] as $stats) {
if (isset($stats['name']) && $stats['name'] === $selected_player) {
// Apply match type filter
$matchType = $match['matchType'] ?? null;
if ($filter_by_match_type === 'all' || $matchType === $filter_by_match_type) {
// Prepare data for display
$displayMatch = [];
$displayMatch['id'] = $match['id'] ?? null;
$displayMatch['matchType'] = $matchType;
$displayMatch['gameMode'] = $match['gameMode'] ?? 'N/A';
$mapNameRaw = $match['mapName'] ?? 'N/A';
$displayMatch['mapName'] = isset($mapNames[$mapNameRaw]) ? $mapNames[$mapNameRaw] : $mapNameRaw;
$displayMatch['kills'] = $stats['kills'] ?? 'N/A';
$displayMatch['damageDealt'] = isset($stats['damageDealt']) ? number_format($stats['damageDealt'], 0, '.', '') : 'N/A';
$displayMatch['timeSurvived'] = $stats['timeSurvived'] ?? 'N/A';
$displayMatch['winPlace'] = $stats['winPlace'] ?? 'N/A';
$createdAt = $match['createdAt'] ?? null;
$displayMatch['formattedDate'] = 'N/A';
if ($createdAt) {
try {
$date = new DateTime($createdAt);
$date->modify('+1 hours'); // Adjust timezone or add offset as needed
$displayMatch['formattedDate'] = $date->format('m-d H:i:s');
} catch (Exception $e) {
$displayMatch['formattedDate'] = 'Invalid Date';
}
}
$filteredMatches[] = $displayMatch;
}
break; // Found the selected player in this match, move to the next match
}
}
}
if (empty($filteredMatches) && !$dataError) {
$dataError .= " No matches found for " . htmlspecialchars($selected_player) . ($filter_by_match_type !== 'all' ? " with type '" . htmlspecialchars($filter_by_match_type) . "'" : "") . ".";
}
}
} else {
$dataError .= " Cached matches data file not found.";
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
@ -17,84 +118,77 @@ $ogDescription = "Dive into the detailed match stats of DTCH Clan in PUBG. Explo
<main> <main>
<section> <section>
<h2>Match Stats</h2> <h2>Match Stats</h2>
<?php
include './config/config.php';
$players_matches = json_decode(file_get_contents('./data/cached_matches.json'), true); <?php if (trim($dataError)): ?>
$players = json_decode(file_get_contents('./config/clanmembers.json'), true); <p style="color: red;"><?php echo htmlspecialchars(trim($dataError)); ?></p>
<?php endif; ?>
// Display buttons for each player <!-- Player Selection Form -->
echo "<form method='get' action=''>"; <?php if (!empty($clanMembers)): ?>
foreach ($players['clanMembers'] as $player) { <form method="get" action="">
<?php foreach ($clanMembers as $player): ?>
<button type="submit" name="selected_player" value="<?php echo htmlspecialchars($player); ?>" class="btn<?php echo ($player === $selected_player) ? ' active' : ''; ?>">
<?php echo htmlspecialchars($player); ?>
</button>
<?php endforeach; ?>
<?php // Keep filter if player changes ?>
<input type="hidden" name="filter_by_match_type" value="<?php echo htmlspecialchars($filter_by_match_type); ?>">
</form>
<br>
<?php endif; ?>
echo "<button type='submit' name='selected_player' value='$player' class='btn'>$player</button>"; <!-- Match Type Filter Form -->
<?php if ($selected_player): ?>
} <form method="get" action="">
echo "</form><br>"; <?php foreach ($matchTypes as $type): ?>
$selected_player = $_GET['selected_player'] ?? $players['clanMembers'][0]; <input type="submit" name="filter_by_match_type" value="<?php echo htmlspecialchars($type); ?>" class="btn<?php echo ($type === $filter_by_match_type) ? ' active' : ''; ?>">
echo "<form method='get' action=''> <?php endforeach; ?>
<input type='submit' name='filter_by_match_type' value='all' class='btn'> <input type="hidden" name="selected_player" value="<?php echo htmlspecialchars($selected_player); ?>">
<input type='submit' name='filter_by_match_type' value='airoyale' class='btn'> </form>
<input type='submit' name='filter_by_match_type' value='official' class='btn'> <br>
<input type='submit' name='filter_by_match_type' value='custom' class='btn'> <?php endif; ?>
<input type='submit' name='filter_by_match_type' value='event' class='btn'>
<input type='submit' name='filter_by_match_type' value='competitive' class='btn'>
<input type='hidden' name='selected_player' value='$selected_player'>
</form><br>";
<?php if ($selected_player && !empty($filteredMatches)): ?>
include './includes/mapsmap.php'; <h2>Recent Matches for <?php echo htmlspecialchars($selected_player); ?> (<?php echo htmlspecialchars(ucfirst($filter_by_match_type)); ?>)</h2>
// Display the player's match stats <table border="1" class="sortable">
echo "<h2>Recent Matches for $selected_player</h2>"; <thead>
echo "<table border='1' class='sortable'>"; <tr>
echo "<tr><th>Match Date</th><th>Game Mode</th><th>Match Type</th><th>Map</th><th>Kills</th><th>Damage Dealt</th><th>Time Survived</th><th>win Place</th></tr>"; <th>Match Date</th>
foreach ($players_matches as $match) { <th>Game Mode</th>
// print_r($match['stats']); <th>Match Type</th>
foreach ($match['stats'] as $stats) { <th>Map</th>
if ($stats['name'] === $selected_player) { <th>Kills</th>
<th>Damage Dealt</th>
<th>Time Survived (s)</th>
if (isset($_GET['filter_by_match_type'])) { <th>Win Place</th>
if ($_GET['filter_by_match_type'] !== 'all' && $match['matchType'] !== $_GET['filter_by_match_type']) { </tr>
continue; </thead>
} <tbody>
} <?php foreach ($filteredMatches as $match):
$date = new DateTime($match['createdAt']); $matchIdLink = 'matchinfo.php?matchid=' . urlencode($match['id'] ?? '');
$date->modify('+1 hours');
$formattedDate = $date->format('m-d H:i:s');
$matchType = $match['matchType'];
$gameMode = $match['gameMode'];
$mapName = isset($mapNames[$match['mapName']]) ? $mapNames[$match['mapName']] : $match['mapName'];
$kills = $stats['kills'];
$damage = number_format($stats['damageDealt'], 0, '.', '');
$timeSurvived = $stats['timeSurvived'];
$winPlace = $stats['winPlace'];
echo "<tr>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $formattedDate . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $gameMode . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $matchType . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $mapName . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $kills . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $damage . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $timeSurvived . "</a></td>
<td><a href='matchinfo.php?matchid=" . $match['id'] . "'>" . $winPlace . "</a></td>
</tr>";
}
}
}
echo "</table><br>";
?> ?>
<tr>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['formattedDate']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['gameMode']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['matchType']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['mapName']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['kills']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['damageDealt']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['timeSurvived']); ?></a></td>
<td><a href="<?php echo $matchIdLink; ?>"><?php echo htmlspecialchars($match['winPlace']); ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<br>
<?php elseif ($selected_player && !$dataError): ?>
<p>No matches found for the selected criteria.</p>
<?php endif; ?>
</section> </section>
</main> </main>
<?php include './includes/footer.php'; ?> <?php include './includes/footer.php'; ?>
</body> </body>
</html> </html>

View file

@ -1,8 +1,8 @@
/* /*
SortTable SortTable
version 2 version 2 (Modernized)
7th April 2007 Original: 7th April 2007, Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ Modernized: [Current Date]
Instructions: Instructions:
Download this file Download this file
@ -10,185 +10,192 @@
Add class="sortable" to any table you'd like to make sortable Add class="sortable" to any table you'd like to make sortable
Click on the headers to sort Click on the headers to sort
Thanks to many, many people for contributions and suggestions.
Licenced as X11: http://www.kryogenix.org/code/browser/licence.html Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
This basically means: do what you want with it. This basically means: do what you want with it.
*/ */
const sorttable = {
DATE_RE: /^(\d{1,2})[\/\.-](\d{1,2})[\/\.-]((\d{2})?\d{2})$/, // Regex for date parsing dd/mm/yyyy or mm/dd/yyyy
SORT_COLUMN_INDEX: 'sorttable_columnindex', // Custom attribute to store column index
var stIsIE = /*@cc_on!@*/false;
sorttable = {
init: function() { init: function() {
// quit if this function has already been called // quit if this function has already been called
if (arguments.callee.done) return; if (this.initialized) return;
// flag this function so we don't do the same thing twice this.initialized = true;
arguments.callee.done = true;
// kill the timer
if (_timer) clearInterval(_timer);
if (!document.createElement || !document.getElementsByTagName) return; if (!document.createElement || !document.getElementsByTagName) return;
sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; document.querySelectorAll('table.sortable').forEach(table => {
this.makeSortable(table);
forEach(document.getElementsByTagName('table'), function(table) {
if (table.className.search(/\bsortable\b/) != -1) {
sorttable.makeSortable(table);
}
}); });
}, },
makeSortable: function(table) { makeSortable: function(table) {
if (table.getElementsByTagName('thead').length == 0) { if (!table.tHead) {
// table doesn't have a tHead. Since it should have, create one and // table doesn't have a tHead. Create one and put the first row in it.
// put the first table row in it. const the = document.createElement('thead');
the = document.createElement('thead'); if (table.rows.length > 0) {
the.appendChild(table.rows[0]); the.appendChild(table.rows[0]);
table.insertBefore(the,table.firstChild); table.insertBefore(the, table.firstChild);
} else {
// Cannot make an empty table sortable
return;
} }
// Safari doesn't support table.tHead, sigh }
// Ensure tHead is correctly referenced (needed for some older browser compatibility, safe otherwise)
if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
if (table.tHead.rows.length != 1) return; // can't cope with two header rows if (table.tHead.rows.length !== 1) return; // Can't cope with multiple header rows
// Sorttable v1 put rows with a class of "sortbottom" at the bottom (as // Handle backwards compatibility for "sortbottom" class (move to tfoot)
// "total" rows, for example). This is B&R, since what you're supposed const sortbottomrows = [];
// to do is put them in a tfoot. So, if there are sortbottom rows, // Use Array.from for iterating HTMLCollection
// for backwards compatibility, move them to tfoot (creating it if needed). Array.from(table.rows).forEach(row => {
sortbottomrows = []; if (row.classList.contains('sortbottom')) {
for (var i=0; i<table.rows.length; i++) { sortbottomrows.push(row);
if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
sortbottomrows[sortbottomrows.length] = table.rows[i];
} }
} });
if (sortbottomrows) {
if (table.tFoot == null) { if (sortbottomrows.length > 0) {
let tfo = table.tFoot;
if (!tfo) {
// table doesn't have a tfoot. Create one. // table doesn't have a tfoot. Create one.
tfo = document.createElement('tfoot'); tfo = document.createElement('tfoot');
table.appendChild(tfo); table.appendChild(tfo);
} }
for (var i=0; i<sortbottomrows.length; i++) { sortbottomrows.forEach(row => {
tfo.appendChild(sortbottomrows[i]); tfo.appendChild(row);
} });
delete sortbottomrows;
} }
// work through each column and calculate its type // Work through each header cell
headrow = table.tHead.rows[0].cells; const headrow = table.tHead.rows[0].cells;
for (var i=0; i<headrow.length; i++) { for (let i = 0; i < headrow.length; i++) {
// manually override the type with a sorttable_type attribute const cell = headrow[i];
if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col // Skip columns with 'sorttable_nosort' class
mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); if (!cell.classList.contains('sorttable_nosort')) {
if (mtch) { override = mtch[1]; } let sortFunc;
if (mtch && typeof sorttable["sort_"+override] == 'function') { // Check for manual override sorttable_type
headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; const match = cell.className.match(/\bsorttable_([a-z0-9]+)\b/);
const override = match ? match[1] : null;
if (override && typeof this[`sort_${override}`] === 'function') {
sortFunc = this[`sort_${override}`];
} else { } else {
headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); sortFunc = this.guessType(table, i);
}
// make it clickable to sort
headrow[i].sorttable_columnindex = i;
headrow[i].sorttable_tbody = table.tBodies[0];
dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
if (this.className.search(/\bsorttable_sorted\b/) != -1) {
// if we're already sorted by this column, just
// reverse the table, which is quicker
sorttable.reverse(this.sorttable_tbody);
this.className = this.className.replace('sorttable_sorted',
'sorttable_sorted_reverse');
this.removeChild(document.getElementById('sorttable_sortfwdind'));
sortrevind = document.createElement('span');
sortrevind.id = "sorttable_sortrevind";
sortrevind.innerHTML = stIsIE ? '&nbsp<font face="webdings">5</font>' : '&nbsp;&#x25B4;';
this.appendChild(sortrevind);
return;
}
if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
// if we're already sorted by this column in reverse, just
// re-reverse the table, which is quicker
sorttable.reverse(this.sorttable_tbody);
this.className = this.className.replace('sorttable_sorted_reverse',
'sorttable_sorted');
this.removeChild(document.getElementById('sorttable_sortrevind'));
sortfwdind = document.createElement('span');
sortfwdind.id = "sorttable_sortfwdind";
sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
this.appendChild(sortfwdind);
return;
} }
// remove sorttable_sorted classes // Make header clickable
theadrow = this.parentNode; cell.sorttable_sortfunction = sortFunc;
forEach(theadrow.childNodes, function(cell) { cell.setAttribute(this.SORT_COLUMN_INDEX, i); // Store index using attribute
if (cell.nodeType == 1) { // an element // Use standard addEventListener
cell.className = cell.className.replace('sorttable_sorted_reverse',''); cell.addEventListener('click', (e) => this.headerClick(e));
cell.className = cell.className.replace('sorttable_sorted','');
}
});
sortfwdind = document.getElementById('sorttable_sortfwdind');
if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
sortrevind = document.getElementById('sorttable_sortrevind');
if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
this.className += ' sorttable_sorted'; // Add visual cue for sortable columns
sortfwdind = document.createElement('span'); cell.style.cursor = 'pointer';
sortfwdind.id = "sorttable_sortfwdind";
sortfwdind.innerHTML = stIsIE ? '&nbsp<font face="webdings">6</font>' : '&nbsp;&#x25BE;';
this.appendChild(sortfwdind);
// build an array to sort. This is a Schwartzian transform thing,
// i.e., we "decorate" each row with the actual sort key,
// sort based on the sort keys, and then put the rows back in order
// which is a lot faster because you only do getInnerText once per row
row_array = [];
col = this.sorttable_columnindex;
rows = this.sorttable_tbody.rows;
for (var j=0; j<rows.length; j++) {
row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
}
/* If you want a stable sort, uncomment the following line */
//sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
/* and comment out this one */
row_array.sort(this.sorttable_sortfunction);
tb = this.sorttable_tbody;
for (var j=0; j<row_array.length; j++) {
tb.appendChild(row_array[j][1]);
}
delete row_array;
});
} }
} }
}, },
guessType: function(table, column) { headerClick: function(e) {
// guess the type of a column based on its first non-blank row const cell = e.currentTarget; // The header cell that was clicked
sortfn = sorttable.sort_alpha; const table = cell.closest('table');
for (var i=0; i<table.tBodies[0].rows.length; i++) { const columnIndex = parseInt(cell.getAttribute(this.SORT_COLUMN_INDEX), 10);
text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); const tbody = table.tBodies[0];
if (text != '') {
if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { if (!tbody || isNaN(columnIndex)) return; // Safety check
return sorttable.sort_numeric;
} const sortFunction = cell.sorttable_sortfunction;
// check for a date: dd/mm/yyyy or dd/mm/yy const isSorted = cell.classList.contains('sorttable_sorted');
// can have / or . or - as separator const isSortedReverse = cell.classList.contains('sorttable_sorted_reverse');
// can be mm/dd as well
possdate = text.match(sorttable.DATE_RE) // Function to update sort indicators
if (possdate) { const updateIndicator = (targetCell, direction) => {
// looks like a date // Remove existing indicators
first = parseInt(possdate[1]); targetCell.querySelectorAll('.sorttable_sortindicator').forEach(span => span.remove());
second = parseInt(possdate[2]); // Add new indicator
if (first > 12) { const indicator = document.createElement('span');
// definitely dd/mm indicator.className = 'sorttable_sortindicator';
return sorttable.sort_ddmm; indicator.innerHTML = direction === 'forward' ? '&nbsp;&#x25BE;' : '&nbsp;&#x25B4;'; // Down / Up arrow
} else if (second > 12) { targetCell.appendChild(indicator);
return sorttable.sort_mmdd; };
// Remove sorting classes and indicators from all headers in this row
cell.parentNode.querySelectorAll('th, td').forEach(siblingCell => {
siblingCell.classList.remove('sorttable_sorted', 'sorttable_sorted_reverse');
siblingCell.querySelectorAll('.sorttable_sortindicator').forEach(span => span.remove());
});
if (isSorted) {
// If already sorted by this column, just reverse the table body
this.reverse(tbody);
cell.classList.add('sorttable_sorted_reverse');
updateIndicator(cell, 'reverse');
} else if (isSortedReverse) {
// If sorted reverse, sort forward again (effectively re-reversing)
// This requires a full sort, not just reversing the current order
this.fullSort(tbody, columnIndex, sortFunction);
cell.classList.add('sorttable_sorted');
updateIndicator(cell, 'forward');
} else { } else {
// looks like a date, but we can't tell which, so assume // New sort
// that it's dd/mm (English imperialism!) and keep looking this.fullSort(tbody, columnIndex, sortFunction);
sortfn = sorttable.sort_ddmm; cell.classList.add('sorttable_sorted');
updateIndicator(cell, 'forward');
}
},
fullSort: function(tbody, columnIndex, sortFunction) {
// Build an array to sort (Schwartzian transform)
const rowArray = [];
Array.from(tbody.rows).forEach(row => {
const cell = row.cells[columnIndex];
const sortKey = this.getInnerText(cell);
rowArray.push([sortKey, row]);
});
// Sort the array using the determined sort function
rowArray.sort(sortFunction);
// Append rows back to the tbody in the new order
rowArray.forEach(item => {
tbody.appendChild(item[1]);
});
},
guessType: function(table, column) {
// Guess the type of a column based on its first non-blank row
let sortfn = this.sort_alpha; // Default to alpha sort
if (!table.tBodies || !table.tBodies[0]) return sortfn; // No body to guess from
const tbody = table.tBodies[0];
for (let i = 0; i < tbody.rows.length; i++) {
const cell = tbody.rows[i].cells[column];
if (!cell) continue; // Skip if cell doesn't exist
const text = this.getInnerText(cell);
if (text !== '') {
// Check for numeric types (including currency and percentages)
if (text.match(/^-?[\£$¤]?[\d,.]+%?$/)) {
return this.sort_numeric;
}
// Check for a date: dd/mm/yyyy or dd/mm/yy or mm/dd/yyyy etc.
const possdate = text.match(this.DATE_RE);
if (possdate) {
// Looks like a date
const first = parseInt(possdate[1], 10);
const second = parseInt(possdate[2], 10);
if (first > 12) {
// Definitely dd/mm
return this.sort_ddmm;
} else if (second > 12) {
// Definitely mm/dd
return this.sort_mmdd;
} else {
// Ambiguous (e.g., 01/02/2023). Default to dd/mm but continue checking other rows.
// If a later row is unambiguously mm/dd, that will take precedence.
sortfn = this.sort_ddmm;
} }
} }
} }
@ -197,298 +204,121 @@ sorttable = {
}, },
getInnerText: function(node) { getInnerText: function(node) {
// gets the text we want to use for sorting for a cell. // Gets the text we want to use for sorting for a cell.
// strips leading and trailing whitespace. // Strips leading and trailing whitespace.
// this is *not* a generic getInnerText function; it's special to sorttable. // Special handling for custom key attribute and input fields.
// for example, you can override the cell text with a customkey attribute.
// it also gets .value for <input> fields.
if (!node) return ""; if (!node) return "";
hasInputs = (typeof node.getElementsByTagName == 'function') && // Check for custom sort key attribute first
node.getElementsByTagName('input').length; const customKey = node.getAttribute("sorttable_customkey");
if (customKey != null) {
return customKey;
}
if (node.getAttribute("sorttable_customkey") != null) { // Handle input fields
return node.getAttribute("sorttable_customkey"); if (node.tagName === 'INPUT' && node.value) {
return node.value.trim();
} }
else if (typeof node.textContent != 'undefined' && !hasInputs) {
return node.textContent.replace(/^\s+|\s+$/g, ''); // Use textContent for modern browsers (strips tags)
} if (typeof node.textContent !== 'undefined') {
else if (typeof node.innerText != 'undefined' && !hasInputs) { return node.textContent.trim();
return node.innerText.replace(/^\s+|\s+$/g, '');
}
else if (typeof node.text != 'undefined' && !hasInputs) {
return node.text.replace(/^\s+|\s+$/g, '');
}
else {
switch (node.nodeType) {
case 3:
if (node.nodeName.toLowerCase() == 'input') {
return node.value.replace(/^\s+|\s+$/g, '');
}
case 4:
return node.nodeValue.replace(/^\s+|\s+$/g, '');
break;
case 1:
case 11:
var innerText = '';
for (var i = 0; i < node.childNodes.length; i++) {
innerText += sorttable.getInnerText(node.childNodes[i]);
}
return innerText.replace(/^\s+|\s+$/g, '');
break;
default:
return '';
} }
// Fallback (might include tags in older environments, less likely needed now)
if (typeof node.innerText !== 'undefined') {
return node.innerText.trim();
} }
// Recursive fallback for complex node structures (rarely needed with textContent)
let innerText = '';
Array.from(node.childNodes).forEach(child => {
innerText += this.getInnerText(child);
});
return innerText.trim();
}, },
reverse: function(tbody) { reverse: function(tbody) {
// reverse the rows in a tbody // Reverse the rows in a tbody
newrows = []; const rows = Array.from(tbody.rows);
for (var i=0; i<tbody.rows.length; i++) { rows.reverse().forEach(row => tbody.appendChild(row));
newrows[newrows.length] = tbody.rows[i];
}
for (var i=newrows.length-1; i>=0; i--) {
tbody.appendChild(newrows[i]);
}
delete newrows;
}, },
/* sort functions /* === Sort Functions ===
each sort function takes two parameters, a and b Each sort function takes two parameters, a and b (arrays from fullSort: [sortKey, rowElement])
you are comparing a[0] and b[0] */ Compare a[0] and b[0]
sort_numeric: function(a,b) { */
aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); sort_numeric: function(a, b) {
if (isNaN(aa)) aa = 0; // Clean string, parse float, default to 0 if NaN
bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); const aa = parseFloat(String(a[0]).replace(/[^0-9.-]/g, '')) || 0;
if (isNaN(bb)) bb = 0; const bb = parseFloat(String(b[0]).replace(/[^0-9.-]/g, '')) || 0;
return aa-bb; return aa - bb;
}, },
sort_alpha: function(a,b) {
if (a[0]==b[0]) return 0; sort_alpha: function(a, b) {
if (a[0]<b[0]) return -1; const strA = String(a[0]).toLowerCase();
return 1; const strB = String(b[0]).toLowerCase();
}, if (strA === strB) return 0;
sort_ddmm: function(a,b) { if (strA < strB) return -1;
mtch = a[0].match(sorttable.DATE_RE);
y = mtch[3]; m = mtch[2]; d = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt1 = y+m+d;
mtch = b[0].match(sorttable.DATE_RE);
y = mtch[3]; m = mtch[2]; d = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt2 = y+m+d;
if (dt1==dt2) return 0;
if (dt1<dt2) return -1;
return 1;
},
sort_mmdd: function(a,b) {
mtch = a[0].match(sorttable.DATE_RE);
y = mtch[3]; d = mtch[2]; m = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt1 = y+m+d;
mtch = b[0].match(sorttable.DATE_RE);
y = mtch[3]; d = mtch[2]; m = mtch[1];
if (m.length == 1) m = '0'+m;
if (d.length == 1) d = '0'+d;
dt2 = y+m+d;
if (dt1==dt2) return 0;
if (dt1<dt2) return -1;
return 1; return 1;
}, },
// Helper for date sorting
_parseDate: function(text, format) {
const match = text.match(sorttable.DATE_RE);
if (!match) return 0; // Or handle as invalid date
let year = parseInt(match[3], 10);
let month, day;
if (format === 'ddmm') {
day = parseInt(match[1], 10);
month = parseInt(match[2], 10);
} else { // mmdd
month = parseInt(match[1], 10);
day = parseInt(match[2], 10);
}
// Handle 2-digit years (assume 20xx or 19xx)
if (match[4]) { // If year had only 2 digits initially
year += (year < 70 ? 2000 : 1900); // Adjust century (adjust threshold if needed)
}
// Pad month and day for consistent string comparison YYYYMMDD
const mm = String(month).padStart(2, '0');
const dd = String(day).padStart(2, '0');
return parseInt(`${year}${mm}${dd}`, 10);
},
sort_ddmm: function(a, b) {
const dt1 = sorttable._parseDate(a[0], 'ddmm');
const dt2 = sorttable._parseDate(b[0], 'ddmm');
return dt1 - dt2;
},
sort_mmdd: function(a, b) {
const dt1 = sorttable._parseDate(a[0], 'mmdd');
const dt2 = sorttable._parseDate(b[0], 'mmdd');
return dt1 - dt2;
},
// shaker_sort (stable sort) is generally not needed as Array.prototype.sort
// is stable in modern JavaScript engines (ES2019+). Kept for reference if needed.
/*
shaker_sort: function(list, comp_func) { shaker_sort: function(list, comp_func) {
// A stable sort function to allow multi-level sorting of data // ... (original implementation using let/const) ...
// see: http://en.wikipedia.org/wiki/Cocktail_sort
// thanks to Joseph Nahmias
var b = 0;
var t = list.length - 1;
var swap = true;
while(swap) {
swap = false;
for(var i = b; i < t; ++i) {
if ( comp_func(list[i], list[i+1]) > 0 ) {
var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
swap = true;
} }
} // for */
t--; };
if (!swap) break; // --- Initialization ---
// Use DOMContentLoaded which is more reliable and fires earlier than window.onload
for(var i = t; i > b; --i) { if (document.readyState === 'loading') {
if ( comp_func(list[i], list[i-1]) < 0 ) { document.addEventListener('DOMContentLoaded', () => sorttable.init());
var q = list[i]; list[i] = list[i-1]; list[i-1] = q; } else {
swap = true; // Handle cases where the script is loaded after DOMContentLoaded
} sorttable.init();
} // for
b++;
} // while(swap)
}
} }
/* ******************************************************************
Supporting functions: bundled here to avoid depending on a library
****************************************************************** */
// Dean Edwards/Matthias Miller/John Resig
/* for Mozilla/Opera9 */
if (document.addEventListener) {
document.addEventListener("DOMContentLoaded", sorttable.init, false);
}
/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
var script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete") {
sorttable.init(); // call the onload handler
}
};
/*@end @*/
/* for Safari */
if (/WebKit/i.test(navigator.userAgent)) { // sniff
var _timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState)) {
sorttable.init(); // call the onload handler
}
}, 10);
}
/* for other browsers */
window.onload = sorttable.init;
// written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini
// http://dean.edwards.name/weblog/2005/10/add-event/
function dean_addEvent(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else {
// assign each event handler a unique ID
if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
// create a hash table of event types for the element
if (!element.events) element.events = {};
// create a hash table of event handlers for each element/event pair
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
// store the existing event handler (if there is one)
if (element["on" + type]) {
handlers[0] = element["on" + type];
}
}
// store the event handler in the hash table
handlers[handler.$$guid] = handler;
// assign a global event handler to do all the work
element["on" + type] = handleEvent;
}
};
// a counter used to create unique IDs
dean_addEvent.guid = 1;
function removeEvent(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
}
};
function handleEvent(event) {
var returnValue = true;
// grab the event object (IE uses a global event object)
event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
// get a reference to the hash table of event handlers
var handlers = this.events[event.type];
// execute each event handler
for (var i in handlers) {
this.$$handleEvent = handlers[i];
if (this.$$handleEvent(event) === false) {
returnValue = false;
}
}
return returnValue;
};
function fixEvent(event) {
// add W3C standard event methods
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
};
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
}
// Dean's forEach: http://dean.edwards.name/base/forEach.js
/*
forEach, version 1.0
Copyright 2006, Dean Edwards
License: http://www.opensource.org/licenses/mit-license.php
*/
// array-like enumeration
if (!Array.forEach) { // mozilla already supports this
Array.forEach = function(array, block, context) {
for (var i = 0; i < array.length; i++) {
block.call(context, array[i], i, array);
}
};
}
// generic enumeration
Function.prototype.forEach = function(object, block, context) {
for (var key in object) {
if (typeof this.prototype[key] == "undefined") {
block.call(context, object[key], key, object);
}
}
};
// character enumeration
String.forEach = function(string, block, context) {
Array.forEach(string.split(""), function(chr, index) {
block.call(context, chr, index, string);
});
};
// globally resolve forEach enumeration
var forEach = function(object, block, context) {
if (object) {
var resolve = Object; // default
if (object instanceof Function) {
// functions have a "length" property
resolve = Function;
} else if (object.forEach instanceof Function) {
// the object implements a custom forEach method so use that
object.forEach(block, context);
return;
} else if (typeof object == "string") {
// the object is a string
resolve = String;
} else if (typeof object.length == "number") {
// the object is array-like
resolve = Array;
}
resolve.forEach(object, block, context);
}
};

View file

@ -1,208 +1,215 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Get in-depth insights into recent PUBG matches. Discover detailed match information including player stats, game modes, match types, and map names. Updated regularly to provide the latest and most comprehensive match data for PUBG enthusiasts."; $ogDescription = "Get in-depth insights into recent PUBG matches. Discover detailed match information including player stats, game modes, match types, and map names. Updated regularly to provide the latest and most comprehensive match data for PUBG enthusiasts.";
?>
// Include map names mapping
include './includes/mapsmap.php'; // Contains the $mapNames array
<?php $matchId = $_GET['matchid'] ?? null;
// Read the JSON file $matchDetails = null;
$jsonData = file_get_contents('data/player_matches.json'); $killStats = [];
$playersData = json_decode($jsonData, true); $participantStats = [];
$dataError = '';
$updateMessage = '';
// Combine matches from all players if ($matchId) {
$allMatches = []; $matchFilename = "data/matches/" . basename($matchId) . ".json"; // Use basename for security
foreach ($playersData as $player) {
foreach ($player['player_matches'] as $match) { if (file_exists($matchFilename)) {
$match['playername'] = $player['playername']; // Add playername to each match for reference $matchJsonData = file_get_contents($matchFilename);
$allMatches[] = $match; $matchData = json_decode($matchJsonData, true);
if (is_array($matchData) && isset($matchData['data']['attributes']) && isset($matchData['included'])) {
$matchDetails = $matchData['data']['attributes'];
$matchDetails['id'] = $matchData['data']['id']; // Add id to details array
// Prepare participant stats
foreach ($matchData['included'] as $includedItem) {
if ($includedItem['type'] == "participant") {
$participantStats[] = $includedItem['attributes']['stats'];
} }
}
// Check for killstats files
$killstatsDirectory = 'data/killstats/';
$killstatsPrefix = $matchData['data']['id'];
$killstatsFiles = glob($killstatsDirectory . $killstatsPrefix . '_*.json'); // More specific glob pattern
if (count($killstatsFiles) == 0) {
// Calculate and display the "check back later" message
try {
$currentTime = new DateTime('now', new DateTimeZone('UTC')); // Use UTC for consistency
$minutes = intval($currentTime->format('i'));
$minutesToNextUpdate = 30 - ($minutes % 30);
if ($minutesToNextUpdate === 30) $minutesToNextUpdate = 0; // Already on the half hour
if ($minutesToNextUpdate > 0) {
$updateMessage = "Kill stats are processing. Check back in $minutesToNextUpdate minutes. Data is updated every half hour.";
} else {
$updateMessage = "Kill stats data is updating, please check back shortly.";
}
} catch (Exception $e) {
$updateMessage = "Could not determine update time."; // Handle potential DateTime errors
}
} else {
// Process killstats files
foreach ($killstatsFiles as $file) {
$killJsonData = json_decode(file_get_contents($file), true);
if (is_array($killJsonData) && isset($killJsonData['stats'])) {
$playerName = $killJsonData['stats']['playername'] ?? 'Unknown';
// Find corresponding participant for additional stats
$totalDamage = 'N/A';
$rank = 'N/A';
$dbnos = 'N/A';
foreach ($participantStats as $pStat) {
if (($pStat['name'] ?? null) === $playerName) {
$totalDamage = $pStat['damageDealt'] ?? 'N/A';
$rank = $pStat['winPlace'] ?? 'N/A';
$dbnos = $pStat['DBNOs'] ?? 'N/A';
break;
}
}
$killStats[] = [
'playername' => $playerName,
'humankills' => $killJsonData['stats']['humankills'] ?? 'N/A',
'HumanDmg' => $killJsonData['stats']['HumanDmg'] ?? 'N/A',
'kills' => $killJsonData['stats']['kills'] ?? 'N/A',
'totalDamage' => $totalDamage,
'rank' => $rank,
'DBNOs' => $dbnos
];
}
}
}
} else {
$dataError = "Error decoding or invalid structure in match JSON file.";
}
} else {
$dataError = "Match data file not found for the given match ID.";
}
} else {
$dataError = "No match ID provided.";
} }
// Sort matches by createdAt date
usort($allMatches, function ($a, $b) {
return strtotime($b['createdAt']) - strtotime($a['createdAt']);
});
// Get the last 5 matches
$lastMatches = array_slice($allMatches, 0, 8);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
include './includes/navigation.php'; include './includes/navigation.php';
include './includes/header.php'; include './includes/header.php';
?> ?>
<main> <main>
<section> <section>
<h2>Match info</h2> <h2>Match Info</h2>
<?php if ($dataError): ?>
<p style="color: red;"><?php echo htmlspecialchars($dataError); ?></p>
<?php elseif ($matchDetails):
$mapNameRaw = $matchDetails['mapName'] ?? 'N/A';
$mapDisplayName = htmlspecialchars(isset($mapNames[$mapNameRaw]) ? $mapNames[$mapNameRaw] : $mapNameRaw);
?>
<h3>Match Details</h3>
<table class='sortable'>
<thead>
<tr><th>Match Type</th><th>Game Mode</th><th>Duration (s)</th><th>Map</th><th>Date</th><th>ID</th></tr>
</thead>
<tbody>
<tr>
<td><?php echo htmlspecialchars($matchDetails['matchType'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($matchDetails['gameMode'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($matchDetails['duration'] ?? 'N/A'); ?></td>
<td><?php echo $mapDisplayName; ?></td>
<td><?php echo htmlspecialchars($matchDetails['createdAt'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($matchDetails['id'] ?? 'N/A'); ?></td>
</tr>
</tbody>
</table>
<br>
<h3>Kill Stats</h3>
<?php if ($updateMessage): ?>
<?php <p><?php echo htmlspecialchars($updateMessage); ?></p>
<?php elseif (!empty($killStats)): ?>
include './includes/mapsmap.php'; <table class='sortable'>
// Check if a match ID is provided in the GET request <thead>
if (isset($_GET['matchid'])) { <tr>
$matchId = $_GET['matchid'];
$filename = "data/matches/" . $matchId . ".json";
// Check if the JSON file for the given match ID exists
if (file_exists($filename)) {
// Read and decode the JSON file
$jsonData = json_decode(file_get_contents($filename), true);
$matchinfo = $jsonData['data']['attributes'];
$matchdata = $jsonData['data'];
echo "<table class='sortable'><tr><th>matchType</th><th>gameMode</th><th>duration</th><th>mapName</th><th>createdAt</th><th>id</th></tr>";
echo "<tr>";
echo "<td>" . htmlspecialchars($matchinfo['matchType']) . "</td>";
echo "<td>" . htmlspecialchars($matchinfo['gameMode']) . "</td>";
echo "<td>" . htmlspecialchars($matchinfo['duration']) . "</td>";
echo "<td>" . htmlspecialchars(isset($mapNames[$matchinfo['mapName']]) ? $mapNames[$matchinfo['mapName']] : $matchinfo['mapName']) . "</td>";
echo "<td>" . htmlspecialchars($matchinfo['createdAt']) . "</td>";
echo "<td>" . htmlspecialchars($matchdata['id']) . "</td>";
echo "</tr>";
echo "</table>";
$directory = 'data/killstats/';
$prefix = $matchdata['id'];
$files = glob($directory . $prefix . '*');
if (count($files) == 0) {
// Get current time
$currentTime = new DateTime();
$minutes = intval($currentTime->format('i'));
// Calculate minutes to next update
$minutesToNextUpdate = 30 - ($minutes % 30);
if ($minutesToNextUpdate === 30) {
// If it's exactly on the hour or half-hour, set the next update to 30 minutes
$minutesToNextUpdate = 0;
}
// Display the message
if ($minutesToNextUpdate > 0) {
echo "Check back in $minutesToNextUpdate minutes. Data is updated every half hour.";
} else {
echo "Data is updating, please check back shortly.";
}
} else {
echo "<table class='sortable'>";
echo "<tr>
<th>Player Name</th> <th>Player Name</th>
<th>humankills</th> <th>Human Kills</th>
<th>HumanDmg </th> <th>Human Dmg</th>
<th>Kills</th> <th>Total Kills</th>
<th>Total Damage</th> <th>Total Damage</th>
<th>Rank</th> <th>Rank</th>
<th>DBNOs</th> <th>DBNOs</th>
</tr>"; </tr>
</thead>
<tbody>
<?php foreach ($killStats as $stat): ?>
<tr>
<td><?php echo htmlspecialchars($stat['playername']); ?></td>
<td><?php echo htmlspecialchars($stat['humankills']); ?></td>
<td><?php echo htmlspecialchars($stat['HumanDmg']); ?></td>
<td><?php echo htmlspecialchars($stat['kills']); ?></td>
<td><?php echo htmlspecialchars(is_numeric($stat['totalDamage']) ? number_format($stat['totalDamage'], 0) : $stat['totalDamage']); ?></td>
<td><?php echo htmlspecialchars($stat['rank']); ?></td>
<td><?php echo htmlspecialchars($stat['DBNOs']); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<br>
<?php else: ?>
<p>No kill stats available for this match.</p>
<?php endif; ?>
foreach ($files as $file) {
$jsonData_individual_player = json_decode(file_get_contents($file), true);
$individualPlayerName = $jsonData_individual_player['stats']['playername'];
// Search for the player in $jsonData['included'] to find damageDealt <h3>All Participants</h3>
$damageDealt = 0; <?php if (!empty($participantStats)): ?>
foreach ($jsonData['included'] as $includedItem) { <table class='sortable'>
if ($includedItem['type'] == "participant") { <thead>
$playerStats = $includedItem['attributes']['stats']; <tr>
if ($individualPlayerName == $playerStats['name']) {
$damageDealt = $playerStats['damageDealt'];
$rank = $playerStats['winPlace'];
$DBNOs = $playerStats['DBNOs'];
break;
}
}
}
echo "<tr>";
echo "<td>" . htmlspecialchars($individualPlayerName) . "</td>";
echo "<td>" . htmlspecialchars($jsonData_individual_player['stats']['humankills']) . "</td>";
echo "<td>" . htmlspecialchars($jsonData_individual_player['stats']['HumanDmg']) . "</td>";
echo "<td>" . htmlspecialchars($jsonData_individual_player['stats']['kills']) . "</td>";
echo "<td>" . htmlspecialchars($damageDealt) . "</td>";
echo "<td>" . htmlspecialchars($rank) . "</td>";
echo "<td>" . htmlspecialchars($DBNOs) . "</td>";
echo "</tr>";
}
echo "</table>";
}
echo "<table class='sortable'>";
echo "<tr>
<th>Player Name</th> <th>Player Name</th>
<th>Sort</th> <th>Type</th>
<th>Kills</th> <th>Kills</th>
<th>Damage Dealt</th> <th>Damage Dealt</th>
<th>Time Survived</th> <th>Time Survived (s)</th>
<th>Rank</th> <th>Rank</th>
<th>Revs</th> <th>Revs</th>
<th>DBNOs</th> <th>DBNOs</th>
<th>Assists</th> <th>Assists</th>
</tr>
</tr>"; </thead>
foreach ($jsonData['included'] as $includedItem) { <tbody>
if ($includedItem['type'] == "participant") { <?php foreach ($participantStats as $pStat):
$playerStats = $includedItem['attributes']['stats']; $isBot = (isset($pStat['playerId']) && substr($pStat['playerId'], 0, 2) === 'ai');
if (substr($playerStats['playerId'], 0, 2) !== 'ai') { $playerName = htmlspecialchars($pStat['name'] ?? 'N/A');
// Create links for each stat $playerLink = 'https://pubg.op.gg/user/' . urlencode($pStat['name'] ?? '');
echo "<tr>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['name']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'> Human </a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['kills']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['HumanDmg']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['timeSurvived']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['winPlace']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['revives']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['DBNOs']) . "</a></td>";
echo "<td><a href='https://pubg.op.gg/user/" . urlencode($playerStats['name']) . "' target='_blank'>" . htmlspecialchars($playerStats['assists']) . "</a></td>";
echo "</tr>";
} else {
// Display without link
echo "<tr>";
echo "<td>" . htmlspecialchars($playerStats['name']) . "</td>";
echo "<td>Bot</a></td>";
echo "<td>" . htmlspecialchars($playerStats['kills']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['damageDealt']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['timeSurvived']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['winPlace']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['revives']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['DBNOs']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['headshotKills']) . "</td>";
echo "<td>" . htmlspecialchars($playerStats['assists']) . "</td>";
echo "</tr>";
}
}
}
echo "</table>";
} else {
echo "JSON file not found for the given match ID.";
}
} else {
echo "No match ID provided.";
}
?> ?>
<tr>
<td><?php echo $isBot ? $playerName : "<a href='{$playerLink}' target='_blank'>{$playerName}</a>"; ?></td>
<td><?php echo $isBot ? 'Bot' : 'Human'; ?></td>
<td><?php echo htmlspecialchars($pStat['kills'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars(isset($pStat['damageDealt']) ? number_format($pStat['damageDealt'], 0) : 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pStat['timeSurvived'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pStat['winPlace'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pStat['revives'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pStat['DBNOs'] ?? 'N/A'); ?></td>
<td><?php echo htmlspecialchars($pStat['assists'] ?? 'N/A'); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table> </table>
<?php else: ?>
<p>No participant data available for this match.</p>
<?php endif; ?>
<?php endif; // End check for $matchDetails ?>
</section> </section>
</main> </main>
<?php include './includes/footer.php'; ?> <?php include './includes/footer.php'; ?>
</body> </body>
</html> </html>

View file

@ -1,60 +1,133 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Check out the top 20 PUBG player rankings in key performance categories! Explore leaderboards for metrics like damage dealt, headshot kills, and more across different game modes. Stay on top of the competitive scene and see where you or your favorite players stand in our regularly updated stats."; $ogDescription = "Check out the top 20 PUBG player rankings in key performance categories! Explore leaderboards for metrics like damage dealt, headshot kills, and more across different game modes. Stay on top of the competitive scene and see where you or your favorite players stand in our regularly updated stats.";
// Include config if it exists
$configPath = './config/config.php';
if (file_exists($configPath)) {
include $configPath;
}
$players_data = null;
$topPlayersByAttribute = [];
$dataError = '';
$lastUpdated = 'N/A';
// Define available modes (expanded for consistency, though only some are used in the form below)
$availableModes = ['solo', 'duo', 'squad', 'solo-fpp', 'duo-fpp', 'squad-fpp'];
// Attributes to display leaderboards for
$attributes = ['wins', 'top10s', 'kills', 'dBNOs', 'damageDealt', 'headshotKills', 'roadKills', 'teamKills', 'roundMostKills'];
// Determine selected game mode (using GET)
$selected_mode = isset($_GET['game_mode']) && in_array($_GET['game_mode'], $availableModes) ? $_GET['game_mode'] : 'squad';
// Load player lifetime data
$dataPath = './data/player_lifetime_data.json';
if (file_exists($dataPath)) {
$jsonData = file_get_contents($dataPath);
$players_data = json_decode($jsonData, true);
if (!is_array($players_data)) {
$dataError = "Error decoding player lifetime data.";
$players_data = null; // Ensure it's null if decoding failed
} else {
$lastUpdated = htmlspecialchars($players_data['updated'] ?? 'N/A');
// Check if the selected mode exists in the data
if (isset($players_data[$selected_mode]) && is_array($players_data[$selected_mode])) {
// Prepare sorted data for each attribute
foreach ($attributes as $attribute) {
$currentModeData = $players_data[$selected_mode]; // Work with a copy for sorting
// Sort players based on the current attribute (descending)
uasort($currentModeData, function ($a, $b) use ($attribute) {
$account_id_a = array_key_first($a);
$account_id_b = array_key_first($b);
// Use null coalescing operator for safety
$stat_a = $a[$account_id_a][$attribute] ?? 0;
$stat_b = $b[$account_id_b][$attribute] ?? 0;
return $stat_b <=> $stat_a; // Sort descending
});
// Get top 20 players for this attribute
$topPlayersByAttribute[$attribute] = array_slice($currentModeData, 0, 20, true);
}
if (empty($topPlayersByAttribute)) {
$dataError = "No player stats found to generate leaderboards for mode: " . htmlspecialchars($selected_mode);
}
} else {
$dataError = "Selected game mode (" . htmlspecialchars($selected_mode) . ") not found in data.";
}
}
} else {
$dataError = "Player lifetime data file not found.";
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
include './includes/navigation.php'; include './includes/navigation.php';
include './includes/header.php'; include './includes/header.php';
?> ?>
<main> <main>
<section> <section>
<h2>User Stats</h2> <h2>Top 20 Player Rankings</h2>
<?php
include './config/config.php';
$players_data = json_decode(file_get_contents('./data/player_lifetime_data.json'), true); <?php if ($dataError): ?>
$selected_mode = isset($_GET['game_mode']) ? $_GET['game_mode'] : 'squad'; <p style="color: red;"><?php echo htmlspecialchars($dataError); ?></p>
<?php endif; ?>
// Form to select game mode <!-- Form to select game mode (using GET) -->
echo "<form method='get' action=''> <form method="get" action="">
<input type='submit' name='game_mode' value='solo' class='btn'> <?php // Only show buttons for modes relevant to this page if desired, or keep all available modes ?>
<input type='submit' name='game_mode' value='duo' class='btn'> <input type="submit" name="game_mode" value="solo" class="btn<?php echo ('solo' === $selected_mode) ? ' active' : ''; ?>">
<input type='submit' name='game_mode' value='squad' class='btn'> <input type="submit" name="game_mode" value="duo" class="btn<?php echo ('duo' === $selected_mode) ? ' active' : ''; ?>">
</form><br>"; <input type="submit" name="game_mode" value="squad" class="btn<?php echo ('squad' === $selected_mode) ? ' active' : ''; ?>">
<input type="submit" name="game_mode" value="solo-fpp" class="btn<?php echo ('solo-fpp' === $selected_mode) ? ' active' : ''; ?>">
<input type="submit" name="game_mode" value="duo-fpp" class="btn<?php echo ('duo-fpp' === $selected_mode) ? ' active' : ''; ?>">
<input type="submit" name="game_mode" value="squad-fpp" class="btn<?php echo ('squad-fpp' === $selected_mode) ? ' active' : ''; ?>">
</form>
<br>
// Displaying top 20 comparisons for each attribute <?php if (!empty($topPlayersByAttribute)): ?>
$attributes = ['wins','top10s','kills','dBNOs','damageDealt','headshotKills','roadKills','teamKills','roundMostKills']; <?php foreach ($topPlayersByAttribute as $attribute => $topPlayers): ?>
foreach ($attributes as $attribute) { <h3>Top 20 <?php echo htmlspecialchars($attribute); ?> (<?php echo htmlspecialchars(ucfirst($selected_mode)); ?>)</h3>
echo "<h3>Top 20 $attribute</h3>"; <?php if (empty($topPlayers)): ?>
uasort($players_data[$selected_mode], function ($a, $b) use ($attribute) { <p>No data available for this attribute in the selected mode.</p>
$account_id_a = array_key_first($a); <?php else: ?>
$account_id_b = array_key_first($b); <table border="1">
return $b[$account_id_b][$attribute] <=> $a[$account_id_a][$attribute]; // Sort in descending order <thead>
}); <tr>
<th>Player</th>
echo "<table border='1'>"; <th><?php echo htmlspecialchars($attribute); ?></th>
echo "<tr><th>Player</th><th>$attribute</th></tr>"; </tr>
$count = 0; </thead>
foreach ($players_data[$selected_mode] as $player_name => $player_details) { <tbody>
if ($count++ >= 20) break; // Limit to top 20 players <?php foreach ($topPlayers as $player_name => $player_details):
$account_id = array_key_first($player_details); $account_id = array_key_first($player_details);
echo "<tr><td>$player_name</td><td>{$player_details[$account_id][$attribute]}</td></tr>"; $statValue = $player_details[$account_id][$attribute] ?? 'N/A';
}
echo "</table><br>";
}
echo "Last update " ;
echo $players_data['updated'];
?> ?>
<tr>
<td><?php echo htmlspecialchars($player_name); ?></td>
<td><?php echo htmlspecialchars($statValue); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<br>
<?php endif; ?>
<?php endforeach; ?>
<?php elseif (!$dataError): ?>
<p>No leaderboards to display for the selected mode.</p>
<?php endif; ?>
<p>Last update: <?php echo $lastUpdated; ?></p>
</section> </section>
</main> </main>

View file

@ -1,67 +1,127 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Discover comprehensive average statistics for PUBG players. Dive into player performance across various game modes including solo, duo, and squad, and explore key metrics like Kills, Damage, Headshots, Wins, and Top10s. Stay updated with the latest statistical trends in PUBG gaming."; $ogDescription = "Discover comprehensive average statistics for PUBG players. Dive into player performance across various game modes including solo, duo, and squad, and explore key metrics like Kills, Damage, Headshots, Wins, and Top10s. Stay updated with the latest statistical trends in PUBG gaming.";
?> // Include config if it exists
$configPath = './config/config.php';
if (file_exists($configPath)) {
include $configPath;
}
$players_data = null;
<!DOCTYPE html> $playerAverages = [];
<html lang="en"> $dataError = '';
<?php include './includes/head.php'; ?> $lastUpdated = 'N/A';
<body> $availableModes = ['solo', 'duo', 'squad', 'solo-fpp', 'duo-fpp', 'squad-fpp']; // Define available modes (expanded for consistency)
$metrics = [
<?php
include './includes/navigation.php';
include './includes/header.php';
?>
<main>
<section>
<h2>Average User Stats</h2>
<?php
include './config/config.php';
$players_data = json_decode(file_get_contents('./data/player_lifetime_data.json'), true);
$selected_mode = isset($_POST['game_mode']) ? $_POST['game_mode'] : 'squad';
// Form to select game mode
echo "<form method='post' action=''>
<input type='submit' name='game_mode' value='solo' class='btn'>
<input type='submit' name='game_mode' value='duo' class='btn'>
<input type='submit' name='game_mode' value='squad' class='btn'>
</form><br>";
$metrics = [
'Kills' => 'kills', 'Kills' => 'kills',
'Damage' => 'damageDealt', 'Damage' => 'damageDealt',
'Headshots' => 'headshotKills', 'Headshots' => 'headshotKills',
'Wins' => 'wins', 'Wins' => 'wins',
'Top10s' => 'top10s' 'Top10s' => 'top10s'
]; ];
echo "<table border='1' class='sortable'>"; // Determine selected game mode (using GET for consistency)
echo "<tr><th>Player</th>"; $selected_mode = isset($_GET['game_mode']) && in_array($_GET['game_mode'], $availableModes) ? $_GET['game_mode'] : 'squad';
foreach ($metrics as $display => $metric) {
echo "<th>Average $display</th>";
}
echo "</tr>";
// Load player lifetime data
$dataPath = './data/player_lifetime_data.json';
if (file_exists($dataPath)) {
$jsonData = file_get_contents($dataPath);
$players_data = json_decode($jsonData, true);
if (!is_array($players_data)) {
$dataError = "Error decoding player lifetime data.";
$players_data = null; // Ensure it's null if decoding failed
} else {
$lastUpdated = htmlspecialchars($players_data['updated'] ?? 'N/A');
// Check if the selected mode exists in the data
if (isset($players_data[$selected_mode]) && is_array($players_data[$selected_mode])) {
// Calculate averages for each player in the selected mode
foreach ($players_data[$selected_mode] as $player_name => $player_details) { foreach ($players_data[$selected_mode] as $player_name => $player_details) {
$account_id = array_key_first($player_details); $account_id = array_key_first($player_details);
if ($account_id && isset($player_details[$account_id])) {
$stats = $player_details[$account_id]; $stats = $player_details[$account_id];
$totalGames = $stats['wins'] + $stats['losses']; // Wins + Losses // Ensure necessary stats exist before calculation
$wins = $stats['wins'] ?? 0;
$losses = $stats['losses'] ?? 0;
$totalGames = $wins + $losses;
echo "<tr><td>$player_name</td>"; $averages = [];
foreach ($metrics as $metric) { foreach ($metrics as $metricKey) {
$averageValue = ($totalGames > 0) ? round($stats[$metric] / $totalGames, 2) : 0; $statValue = $stats[$metricKey] ?? 0;
echo "<td>$averageValue</td>"; $averages[$metricKey] = ($totalGames > 0) ? round($statValue / $totalGames, 2) : 0;
} }
echo "</tr>"; $playerAverages[$player_name] = $averages;
} }
}
if (empty($playerAverages)) {
$dataError = "No player stats found to calculate averages for mode: " . htmlspecialchars($selected_mode);
}
} else {
$dataError = "Selected game mode (" . htmlspecialchars($selected_mode) . ") not found in data.";
}
}
} else {
$dataError = "Player lifetime data file not found.";
}
?>
<!DOCTYPE html>
<html lang="en">
<?php include './includes/head.php'; // Includes $ogDescription ?>
<body>
<?php
include './includes/navigation.php';
include './includes/header.php';
?>
<main>
<section>
<h2>Average User Stats</h2>
<?php if ($dataError): ?>
<p style="color: red;"><?php echo htmlspecialchars($dataError); ?></p>
<?php endif; ?>
<!-- Form to select game mode (using GET) -->
<form method="get" action="">
<?php foreach ($availableModes as $mode): ?>
<input type="submit" name="game_mode" value="<?php echo htmlspecialchars($mode); ?>" class="btn<?php echo ($mode === $selected_mode) ? ' active' : ''; ?>">
<?php endforeach; ?>
</form>
<br>
<?php if (!empty($playerAverages)): ?>
<table border="1" class="sortable">
<thead>
<tr>
<th>Player</th>
<?php foreach ($metrics as $display => $metric): ?>
<th>Average <?php echo htmlspecialchars($display); ?></th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($playerAverages as $player_name => $averages): ?>
<tr>
<td><?php echo htmlspecialchars($player_name); ?></td>
<?php foreach ($metrics as $metricKey): ?>
<td><?php echo htmlspecialchars($averages[$metricKey] ?? '0'); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<br>
<?php elseif (!$dataError): ?>
<p>No average stats to display for the selected mode.</p>
<?php endif; ?>
<p>Last update: <?php echo $lastUpdated; ?></p>
echo "</table><br>";
echo "Last update " ;
echo $players_data['updated'];
?>
</section> </section>
</main> </main>

View file

@ -1,147 +1,429 @@
# --- Script Setup ---
Start-Transcript -Path '/var/log/dtch/get_matches.log' -Append Start-Transcript -Path '/var/log/dtch/get_matches.log' -Append
Write-Output "Starting get_matches script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
if ($PSScriptRoot.length -eq 0) { # Determine script root directory reliably
$scriptroot = Get-Location if ($PSScriptRoot) {
} $scriptRoot = $PSScriptRoot
else { } else {
$scriptroot = $PSScriptRoot $scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
Write-Output "Script root identified as: $scriptRoot"
. $scriptroot\..\includes\ps1\lockfile.ps1 # Define paths using Join-Path
new-lock -by "get_matches" $includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
$matchesPath = Join-Path -Path $dataPath -ChildPath "matches"
$matchesArchivePath = Join-Path -Path $matchesPath -ChildPath "archive"
$playerDataJsonPath = Join-Path -Path $dataPath -ChildPath "player_data.json"
$playerMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "player_matches.json"
$cachedMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "cached_matches.json"
# Read the content of the file as a single string # Ensure required directories exist
$fileContent = Get-Content -Path "$scriptroot/../config/config.php" -Raw @( $dataPath, $matchesPath, $matchesArchivePath ) | ForEach-Object {
$players = (Get-Content -Path "$scriptroot/../config/clanmembers.json" | ConvertFrom-Json).clanmembers if (-not (Test-Path -Path $_ -PathType Container)) {
# Use regex to match the apiKey value Write-Warning "Directory not found at '$_'. Attempting to create."
if ($fileContent -match "\`$apiKey\s*=\s*\'([^\']+)\'") {
$apiKey = $matches[1]
}
else {
Write-Output "API Key not found"
}
$headers = @{
'accept' = 'application/vnd.api+json'
'Authorization' = "$apiKey"
}
$player_matches = @()
try {
$player_data = get-content "$scriptroot/../data/player_data.json" | convertfrom-json -Depth 100
}
catch {
Write-Output 'Unable to read file exitin'
exit
}
foreach ($player in $player_data) {
$lastMatches = $player.relationships.matches.data.id #| Select-Object -First 10
$playermatches = @()
foreach ($match in $lastMatches) {
Write-Host "Getting match for $($player.attributes.name) match: $match "
if (Test-Path "$scriptroot/../data/matches/$match.json") {
write-output "Getting $match from cache"
$stats = get-content "$scriptroot/../data/matches/$match.json" | convertfrom-json
}
else {
$stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/matches/$match" -Method GET -Headers $headers
$sortedStats = $stats.included | Sort-Object { $_.attributes.stats.winplace }
$stats.included = $sortedStats
$stats | ConvertTo-Json -Depth 100 | Out-File "$scriptroot/../data/matches/$match.json"
}
$playermatches += [PSCustomObject]@{
stats = $stats.included.ATTRIBUTES.stats | where-object { $_.name -eq $player.attributes.name }
matchType = $stats.data.attributes.matchtype
gameMode = $stats.data.attributes.gameMode
createdAt = $stats.data.attributes.createdAt
mapName = $stats.data.attributes.mapName
telemetry_url = ($stats.included.attributes | Where-Object { $_.name -eq 'telemetry' }).URL
id = $stats.data.id
}
}
$obj = [PSCustomObject]@{
playername = $player.attributes.name
player_matches = $playermatches
}
$player_matches += $obj
}
if (test-path "$scriptroot/../data/player_matches.json") {
try { try {
$old_player_data = get-content "$scriptroot/../data/player_matches.json" | convertfrom-json -Depth 100 New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Successfully created directory: $_"
} catch {
Write-Error "Failed to create directory '$_'. Please check permissions. Error: $($_.Exception.Message)"
Stop-Transcript
exit 1
} }
catch {
Write-Output 'Unable to read file exitin'
exit
} }
$new_ids = ($player_matches.player_matches | where-object { $_.stats.winplace -eq 1 }).id
$old_ids = ($old_player_data.player_matches | where-object { $_.stats.winplace -eq 1 }).id
$new_win_matches = ((Compare-Object -ReferenceObject $old_ids -DifferenceObject $new_ids) | Where-Object { $_.SideIndicator -eq '=>' }).InputObject | Select-Object -Unique
$new_win_matches = $old_player_data.new_win_matches + $new_win_matches | Select-Object -Unique
$player_matches += [PSCustomObject]@{
new_win_matches = $new_win_matches
}
# Nieuwe verloren matches bepalen
$new_loss_ids = ($player_matches.player_matches | Where-Object { $_.stats.winplace -ne 1 }).id
$old_loss_ids = ($old_player_data.player_matches | Where-Object { $_.stats.winplace -ne 1 }).id
$new_loss_matches = ((Compare-Object -ReferenceObject $old_loss_ids -DifferenceObject $new_loss_ids) | Where-Object { $_.SideIndicator -eq '=>' }).InputObject | Select-Object -Unique
$new_loss_matches = $old_player_data.new_loss_matches + $new_loss_matches | Select-Object -Unique
$player_matches += [PSCustomObject]@{
new_loss_matches = $new_loss_matches
}
} }
$currentDateTime = Get-Date # --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
# Get current timezone if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
$currentTimezone = (Get-TimeZone).Id Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
# Format and parse the information into a string exit 1
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
# Output the formatted string
$playermatches += [PSCustomObject]@{
updated = $formattedString
} }
. $lockFilePath
New-Lock -by "get_matches" -ErrorAction Stop # Stop if locking fails
$player_matches | convertto-json -Depth 100 | out-file "$scriptroot/../data/player_matches.json" # --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Configuration Loading ---
$apiKey = $null
$clanMembers = @() # Renamed from $players for clarity
write-output 'Cleaning matches' # Load API Key from config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
# Corrected regex for apiKey
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') {
$apiKey = $matches[1]
Write-Output "API Key loaded successfully."
} else {
Write-Warning "API Key pattern not found in '$phpConfigPath'."
}
} catch {
Write-Warning "Failed to read '$phpConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Config file not found at '$phpConfigPath'."
}
$matchfiles = Get-ChildItem "$scriptroot/../data/matches" -Filter *.json if (-not $apiKey) {
$player_matches_object = @() Write-Error "API Key could not be loaded. Cannot proceed without API Key."
foreach ($file in $matchfiles) { throw "Missing API Key" # Throw to trigger finally block
$filecontent = get-content $file | convertfrom-json
$matchfiledate = $filecontent.data.attributes.createdAt
if ($matchfiledate -lt (get-date).AddMonths(-3)) {
write-output "archiving $matchfiledate"
Move-Item -Path $file -Destination "$scriptroot/../data/matches/archive"
} }
else {
$result = ($filecontent.included | where-object { $_.type -eq 'participant' } | Where-Object { $players -contains $_.attributes.stats.name }) # Load Clan Members from clanmembers.json
$filecontent.data.id $clanMembersJsonPath = Join-Path -Path $configPath -ChildPath "clanmembers.json"
$result.count if (Test-Path -Path $clanMembersJsonPath -PathType Leaf) {
$player_matches_cached = ($filecontent.included | where-object { $_.type -eq 'participant' } | Where-Object { $players -contains $_.attributes.stats.name }).attributes.stats try {
if ($null -ne $player_matches_cached) { $clanMembersData = Get-Content -Path $clanMembersJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop
$player_matches_object += [PSCustomObject]@{ if ($clanMembersData -is [PSCustomObject] -and $clanMembersData.PSObject.Properties.Name -contains 'clanMembers' -and $clanMembersData.clanMembers -is [array]) {
matchType = $filecontent.data.attributes.matchType $clanMembers = $clanMembersData.clanMembers
gameMode = $filecontent.data.attributes.gameMode Write-Output "Clan members loaded successfully. Count: $($clanMembers.Count)"
createdAt = $filecontent.data.attributes.createdAt } else {
mapName = $filecontent.data.attributes.mapName Write-Warning "Invalid structure in '$clanMembersJsonPath'. Expected an object with a 'clanMembers' array."
id = $filecontent.data.id }
stats = @($player_matches_cached) } catch {
Write-Warning "Failed to read or parse '$clanMembersJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Clan members file not found at '$clanMembersJsonPath'."
}
if ($clanMembers.Count -eq 0) {
Write-Warning "No clan members loaded. Proceeding, but cached matches might be incomplete."
# Decide if this is a fatal error or not
}
# --- Helper Function for API Calls (Copied from update_clan_members.ps1) ---
function Invoke-PubgApi {
param(
[Parameter(Mandatory=$true)]
[string]$Uri,
[Parameter(Mandatory=$true)]
[hashtable]$Headers,
[int]$RetryCount = 1,
[int]$RetryDelaySeconds = 61
)
for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
try {
Write-Verbose "Attempting API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
if ($null -ne $response) { Write-Verbose "API call successful."; return $response }
else { Write-Warning "API call to $Uri returned null or empty response."; return $null }
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping for $RetryDelaySeconds seconds before retry..."
Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) { Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"; return $null }
else { Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"; return $null }
} }
} }
write-output "NEW $matchfiledate" return $null
} }
# --- Load Player Data (IDs and Match Lists) ---
$playerData = $null
if (Test-Path -Path $playerDataJsonPath -PathType Leaf) {
try {
# Assuming player_data.json contains an object with a 'data' array property
$playerDataWrapper = Get-Content -Path $playerDataJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -ne $playerDataWrapper -and $playerDataWrapper.PSObject.Properties.Name -contains 'data' -and $playerDataWrapper.data -is [array]) {
$playerData = $playerDataWrapper.data
Write-Output "Successfully loaded player data. Count: $($playerData.Count)"
} else {
Write-Error "Invalid structure in '$playerDataJsonPath'. Expected object with 'data' array. Cannot proceed."
throw "Invalid player data structure."
}
} catch {
Write-Error "Error reading '$playerDataJsonPath': $($_.Exception.Message). Cannot proceed."
throw $_
}
} else {
Write-Error "Player data file not found at '$playerDataJsonPath'. Run update_clan_members.ps1 first. Cannot proceed."
throw "Missing player data file."
}
# --- Fetch and Process Matches for Each Player ---
Write-Output "Fetching and processing matches for $($playerData.Count) players..."
$allPlayerMatchDetails = @() # Store processed match details for all players
$apiHeaders = @{
'accept' = 'application/vnd.api+json'
'Authorization' = "Bearer $apiKey"
}
$matchesFetched = 0
$matchesCached = 0
foreach ($player in $playerData) {
# Validate player structure
if ($null -eq $player.attributes -or $null -eq $player.attributes.name -or $null -eq $player.relationships.matches.data) {
Write-Warning "Skipping player due to missing attributes, name, or match data: $($player | Out-String)"
continue
}
$playerName = $player.attributes.name
$playerMatchIds = $player.relationships.matches.data.id
Write-Output "Processing player: $playerName ($($playerMatchIds.Count) recent matches)"
$currentPlayerMatches = @()
foreach ($matchId in $playerMatchIds) {
if (-not $matchId) { Write-Warning "Skipping null/empty match ID for player $playerName."; continue }
Write-Verbose "Getting match details for $playerName, Match ID: $matchId"
$matchJsonPath = Join-Path -Path $matchesPath -ChildPath "$matchId.json"
$matchStats = $null
# Check cache first
if (Test-Path -Path $matchJsonPath -PathType Leaf) {
Write-Verbose "Getting $matchId from cache."
try {
$matchStats = Get-Content -Path $matchJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $matchStats) { Write-Warning "Failed to parse cached match file: $matchJsonPath" }
else { $matchesCached++ }
} catch {
Write-Warning "Error reading cached match file '$matchJsonPath': $($_.Exception.Message)"
}
}
# Fetch from API if not cached or cache failed
if ($null -eq $matchStats) {
$apiUrl = "https://api.pubg.com/shards/steam/matches/$matchId"
Write-Output "Fetching match $matchId from API..."
$matchStats = Invoke-PubgApi -Uri $apiUrl -Headers $apiHeaders
if ($null -ne $matchStats) {
$matchesFetched++
# Sort included participants by winPlace before saving (optional, but done in original)
try {
if ($null -ne $matchStats.included -and $matchStats.included -is [array]) {
$matchStats.included = $matchStats.included | Sort-Object -Property { $_.attributes.stats.winPlace } -ErrorAction SilentlyContinue
}
} catch { Write-Warning "Could not sort 'included' array for match $matchId." }
# Save to cache
try {
$matchStats | ConvertTo-Json -Depth 100 | Out-File -FilePath $matchJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Verbose "Saved match $matchId to cache."
} catch {
Write-Warning "Failed to save match $matchId to cache '$matchJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Failed to fetch match $matchId from API for player $playerName."
continue # Skip this match if API fetch failed
}
}
# Process the retrieved/cached match stats
if ($null -ne $matchStats -and $null -ne $matchStats.data -and $null -ne $matchStats.included) {
# Find the specific player's stats within the 'included' array
$playerSpecificStats = $null
if ($matchStats.included -is [array]) {
$playerSpecificStats = $matchStats.included |
Where-Object { $_.type -eq 'participant' -and ($_.attributes.stats.name -eq $playerName) } |
Select-Object -First 1 -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
# Find telemetry URL
$telemetryUrl = $null
if ($matchStats.included -is [array]) {
$telemetryAsset = $matchStats.included | Where-Object { $_.type -eq 'asset' -and $_.attributes.name -eq 'telemetry' } | Select-Object -First 1
if ($telemetryAsset) { $telemetryUrl = $telemetryAsset.attributes.URL }
}
if ($null -ne $playerSpecificStats) {
$currentPlayerMatches += [PSCustomObject]@{
stats = $playerSpecificStats # Just the stats object for this player
matchType = $matchStats.data.attributes.matchType
gameMode = $matchStats.data.attributes.gameMode
createdAt = $matchStats.data.attributes.createdAt
mapName = $matchStats.data.attributes.mapName
telemetry_url = $telemetryUrl
id = $matchStats.data.id
}
} else {
Write-Warning "Could not find stats for player $playerName within match $matchId data."
}
} else {
Write-Warning "Match data structure for $matchId is invalid or incomplete after retrieval/caching."
}
} # End foreach matchId
# Add the processed matches for the current player to the main list
$allPlayerMatchDetails += [PSCustomObject]@{
playername = $playerName
player_matches = $currentPlayerMatches # Array of processed match objects for this player
}
Write-Output "Finished processing matches for $playerName. Found $($currentPlayerMatches.Count) valid entries."
} # End foreach player
Write-Output "Finished fetching/processing all matches. API Fetches: $matchesFetched, Cache Hits: $matchesCached."
# --- Compare with Old Data & Identify New Wins/Losses ---
Write-Output "Comparing current matches with old data..."
$oldPlayerMatchData = $null
$newWinMatchesList = @()
$newLossMatchesList = @()
if (Test-Path -Path $playerMatchesJsonPath -PathType Leaf) {
try {
$oldPlayerMatchData = Get-Content -Path $playerMatchesJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $oldPlayerMatchData) {
Write-Warning "Failed to parse old player matches file: $playerMatchesJsonPath. Cannot compare for new wins/losses."
} else {
Write-Output "Successfully loaded old player matches data for comparison."
# Extract all current match IDs and old match IDs safely
$currentMatchIds = ($allPlayerMatchDetails.player_matches.id | Select-Object -Unique)
$oldMatchIds = $null
if ($oldPlayerMatchData -is [array]) {
$oldMatchIds = ($oldPlayerMatchData.player_matches.id | Select-Object -Unique)
} else {
Write-Warning "Old player matches data is not in the expected array format."
}
if ($null -ne $oldMatchIds) {
# Compare IDs to find newly added matches
$newMatchIds = (Compare-Object -ReferenceObject $oldMatchIds -DifferenceObject $currentMatchIds | Where-Object { $_.SideIndicator -eq '=>' }).InputObject | Select-Object -Unique
Write-Output "Found $($newMatchIds.Count) new match IDs."
# Identify new wins and losses among the new matches
$newMatchesDetails = $allPlayerMatchDetails.player_matches | Where-Object { $newMatchIds -contains $_.id }
$newWinMatchesList = ($newMatchesDetails | Where-Object { $_.stats.winPlace -eq 1 }).id | Select-Object -Unique
$newLossMatchesList = ($newMatchesDetails | Where-Object { $_.stats.winPlace -ne 1 }).id | Select-Object -Unique
Write-Output "Identified $($newWinMatchesList.Count) new wins and $($newLossMatchesList.Count) new losses."
# Combine with potentially existing lists from old data (if format was correct)
$oldWinMatches = $oldPlayerMatchData | Where-Object { $_.PSObject.Properties.Name -eq 'new_win_matches' } | Select-Object -ExpandProperty new_win_matches
$oldLossMatches = $oldPlayerMatchData | Where-Object { $_.PSObject.Properties.Name -eq 'new_loss_matches' } | Select-Object -ExpandProperty new_loss_matches
if ($oldWinMatches -is [array]) { $newWinMatchesList = ($oldWinMatches + $newWinMatchesList) | Select-Object -Unique }
if ($oldLossMatches -is [array]) { $newLossMatchesList = ($oldLossMatches + $newLossMatchesList) | Select-Object -Unique }
}
}
} catch {
Write-Warning "Error reading or processing old player matches file '$playerMatchesJsonPath': $($_.Exception.Message)"
}
} else {
Write-Output "Old player matches file not found. Cannot compare for new wins/losses."
# If no old file, all current wins/losses are "new" for the first run
$newWinMatchesList = ($allPlayerMatchDetails.player_matches | Where-Object { $_.stats.winPlace -eq 1 }).id | Select-Object -Unique
$newLossMatchesList = ($allPlayerMatchDetails.player_matches | Where-Object { $_.stats.winPlace -ne 1 }).id | Select-Object -Unique
Write-Output "Treating all current wins ($($newWinMatchesList.Count)) and losses ($($newLossMatchesList.Count)) as new."
}
# Add the lists of new wins/losses to the data structure
$allPlayerMatchDetails += [PSCustomObject]@{ new_win_matches = $newWinMatchesList }
$allPlayerMatchDetails += [PSCustomObject]@{ new_loss_matches = $newLossMatchesList }
# Add update timestamp
$currentDateTime = Get-Date
$currentTimezone = (Get-TimeZone).Id
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
$allPlayerMatchDetails += [PSCustomObject]@{ updated = $formattedString }
Write-Output "Added update timestamp and new win/loss lists."
# --- Save Updated Player Matches Data ---
try {
$allPlayerMatchDetails | ConvertTo-Json -Depth 100 | Out-File -FilePath $playerMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Updated player matches data saved to '$playerMatchesJsonPath'"
} catch {
Write-Error "Failed to save updated player matches data to '$playerMatchesJsonPath'. Error: $($_.Exception.Message)"
}
# --- Clean Old Match Files & Create Cached Summary ---
Write-Output "Cleaning old match files and creating cached summary..."
$cachedMatches = @()
$archivedMatchFiles = 0
$processedMatchFiles = 0
$monthsToKeepMatches = -3 # How long to keep individual match files
try {
$matchFiles = Get-ChildItem -Path $matchesPath -Filter *.json -File -ErrorAction SilentlyContinue
if ($matchFiles) {
$archiveThreshold = (Get-Date).AddMonths($monthsToKeepMatches)
Write-Output "Archiving match files older than: $archiveThreshold"
foreach ($file in $matchFiles) {
$processedMatchFiles++
try {
$fileContent = Get-Content -Path $file.FullName | ConvertFrom-Json -Depth 100 -ErrorAction Stop
# Validate essential structure
if ($null -eq $fileContent -or $null -eq $fileContent.data.attributes.createdAt -or $null -eq $fileContent.included) {
Write-Warning "Skipping invalid match file: $($file.Name)"
continue
}
$matchFileDate = $null
try { $matchFileDate = [datetime]$fileContent.data.attributes.createdAt } catch { Write-Warning "Could not parse date in match file $($file.Name)" }
# Archive old files
if ($null -ne $matchFileDate -and $matchFileDate -lt $archiveThreshold) {
Write-Verbose "Archiving match file: $($file.Name)"
Move-Item -Path $file.FullName -Destination $matchesArchivePath -Force -ErrorAction SilentlyContinue
if ($?) { $archivedMatchFiles++ }
else { Write-Warning "Failed to archive match file: $($file.Name)" }
} else {
# Process file for cached summary if it's recent enough
$matchAttributes = $fileContent.data.attributes
$matchId = $fileContent.data.id
# Find stats for clan members within this match
$clanMemberStatsInMatch = @()
if ($fileContent.included -is [array]) {
$clanMemberStatsInMatch = $fileContent.included |
Where-Object { $_.type -eq 'participant' -and $clanMembers -contains $_.attributes.stats.name } |
Select-Object -ExpandProperty attributes | Select-Object -ExpandProperty stats
}
if ($clanMemberStatsInMatch.Count -gt 0) {
$cachedMatches += [PSCustomObject]@{
matchType = $matchAttributes.matchType
gameMode = $matchAttributes.gameMode
createdAt = $matchAttributes.createdAt
mapName = $matchAttributes.mapName
id = $matchId
stats = @($clanMemberStatsInMatch) # Ensure stats is always an array
}
}
}
} catch {
Write-Warning "Error processing match file '$($file.Name)': $($_.Exception.Message)"
}
} # End foreach file
Write-Output "Processed $processedMatchFiles match files. Archived: $archivedMatchFiles."
# Save the cached summary object
if ($cachedMatches.Count -gt 0) {
try {
$cachedMatches | Sort-Object createdAt -Descending | ConvertTo-Json -Depth 100 | Out-File -FilePath $cachedMatchesJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Cached matches summary saved to '$cachedMatchesJsonPath'. Count: $($cachedMatches.Count)"
} catch {
Write-Error "Failed to save cached matches summary to '$cachedMatchesJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Output "No recent matches found containing clan members for cached summary."
# Optionally clear or save an empty array to the cache file
# @() | ConvertTo-Json | Out-File -FilePath $cachedMatchesJsonPath -Encoding UTF8
}
} else {
Write-Output "No match files found in '$matchesPath' to process or clean."
}
} catch {
Write-Warning "Error during match file cleaning/caching process: $($_.Exception.Message)"
}
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
Stop-Transcript
} }
$player_matches_object | Sort-Object createdAt -Descending | convertto-json -Depth 100 | out-file "$scriptroot/../data/cached_matches.json"
remove-lock
Stop-Transcript

View file

@ -1,307 +1,575 @@
# --- Script Setup ---
Start-Transcript -Path '/var/log/dtch/matchparser.log' -Append Start-Transcript -Path '/var/log/dtch/matchparser.log' -Append
Write-Output 'Running from' Write-Output "Starting matchparser script at $(Get-Date)"
(Get-Location).path Write-Output "Running from: $(Get-Location)"
if ($PSScriptRoot.length -eq 0) { # Determine script root directory reliably
$scriptroot = Get-Location if ($PSScriptRoot) {
$scriptRoot = $PSScriptRoot
} else {
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
else { Write-Output "Script root identified as: $scriptRoot"
$scriptroot = $PSScriptRoot
# Define paths using Join-Path
$includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
$killStatsPath = Join-Path -Path $dataPath -ChildPath "killstats"
$archivePath = Join-Path -Path $killStatsPath -ChildPath "archive" # Archive within killstats
$telemetryCachePath = Join-Path -Path $dataPath -ChildPath "telemetry_cache"
$playerMatchesJsonPath = Join-Path -Path $dataPath -ChildPath "player_matches.json"
$lastStatsJsonPath = Join-Path -Path $dataPath -ChildPath "player_last_stats.json"
$archiveDir = Join-Path -Path $dataPath -ChildPath "archive" # Separate archive for player_last_stats
# Ensure required directories exist
@( $dataPath, $killStatsPath, $archivePath, $telemetryCachePath, $archiveDir ) | ForEach-Object {
if (-not (Test-Path -Path $_ -PathType Container)) {
Write-Warning "Directory not found at '$_'. Attempting to create."
try {
New-Item -Path $_ -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Successfully created directory: $_"
} catch {
Write-Error "Failed to create directory '$_'. Please check permissions. Error: $($_.Exception.Message)"
Stop-Transcript
exit 1
}
}
} }
. $scriptroot\..\includes\ps1\lockfile.ps1 # --- Locking ---
new-lock -by "matchparser" $lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
##SETTINGS if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
$monthsback = -3 # how many months back to look for matches Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
##END OF SETTINGS Stop-Transcript
function Get-Change { exit 1
}
. $lockFilePath
New-Lock -by "matchparser" -ErrorAction Stop # Stop if locking fails
# --- Main Logic in Try/Finally for Lock Removal ---
try {
# --- Settings ---
$monthsBack = -3 # How many months back to look for matches for aggregation
Write-Output "Using monthsBack setting: $monthsBack"
# --- Helper Functions ---
function Get-Change {
param ( param (
[double]$OldWinRatio, [double]$OldWinRatio,
[double]$NewWinRatio [double]$NewWinRatio
) )
# Ensure inputs are treated as numbers, default to 0 if null/invalid
$old = if ($OldWinRatio -is [double]) { $OldWinRatio } else { 0.0 }
$new = if ($NewWinRatio -is [double]) { $NewWinRatio } else { 0.0 }
$change = ($OldWinRatio -eq 0) ? (($NewWinRatio -eq 0) ? 0 : $NewWinRatio) : ($NewWinRatio - $OldWinRatio) # Calculate change: If old is 0, change is simply the new ratio. Otherwise, it's the difference.
$change = ($old -eq 0) ? $new : ($new - $old)
return [math]::Round($change, 2) return [math]::Round($change, 2)
} }
function Get-winratio { function Get-WinRatio {
param ( param (
[int]$player_wins, [int]$playerWins,
[int]$player_matches [int]$playerMatches
) )
if ($player_wins -eq 0 -or $player_matches -eq 0) { if ($playerMatches -gt 0) {
$winratio = 0 # Calculate win ratio percentage
return [math]::Round(($playerWins / $playerMatches) * 100, 2) # Round percentage
} else {
return 0.0 # Return 0.0 for consistency if no matches played
} }
else {
$winratio = ($player_wins / $player_matches) * 100
} }
return $winratio
} function Get-KillStatsFromTelemetry {
function get-killstats {
param ( param (
$player_name, [string]$playerName,
$telemetry, [array]$telemetryEvents, # Expecting pre-filtered events
$matchType, [string]$matchType,
$gameMode [string]$gameMode
) )
$LOGPLAYERKILLV2 = $telemetry | where-object { $_._T -eq 'LOGPLAYERKILLV2' }
$kills = $LOGPLAYERKILLV2 | where-object { $_.killer.name -eq $player_name } # Validate input
$deaths = $LOGPLAYERKILLV2 | where-object { $_.victim.name -eq $player_name -and $_.finisher.name.count -ge 1 } if (-not $playerName -or $null -eq $telemetryEvents) {
$HumanDmg = $([math]::Round(($telemetry | Where-Object { $_._T -eq 'LOGPLAYERTAKEDAMAGE' -and $_.attacker.name -eq $player_name -and $_.victim.accountId -notlike "ai.*" -and $_.victim.teamId -ne $_.attacker.teamId } | Measure-Object -Property damage -Sum).Sum)) Write-Warning "Get-KillStatsFromTelemetry: Invalid input (PlayerName or TelemetryEvents)."
return $null
}
# Filter relevant events once
$killEvents = $telemetryEvents | Where-Object { $_._T -eq 'LogPlayerKillV2' }
$damageEvents = $telemetryEvents | Where-Object { $_._T -eq 'LogPlayerTakeDamage' }
# Calculate Kills
$playerKills = $killEvents | Where-Object { $null -ne $_.killer -and $_.killer.name -eq $playerName }
$totalKillsCount = $playerKills.Count
$humanKillsCount = ($playerKills | Where-Object { $null -ne $_.victim -and $_.victim.accountId -notlike 'ai.*' }).Count
$dbnoCount = ($playerKills | Where-Object { $null -ne $_.dBNOId }).Count # Check if DBNO event exists
# Calculate Deaths (where player is victim and finished by someone)
# Note: Finisher check might be complex depending on data structure, adjust if needed
$playerDeaths = $killEvents | Where-Object { $null -ne $_.victim -and $_.victim.name -eq $playerName -and $null -ne $_.finisher }
$deathsCount = $playerDeaths.Count
# Calculate Human Damage Dealt
$humanDamageDealt = 0
try {
$humanDamageEvents = $damageEvents | Where-Object {
$null -ne $_.attacker -and $_.attacker.name -eq $playerName -and
$null -ne $_.victim -and $_.victim.accountId -notlike "ai.*" -and
$_.victim.teamId -ne $_.attacker.teamId
}
if ($humanDamageEvents) {
$humanDamageDealt = ($humanDamageEvents | Measure-Object -Property damage -Sum).Sum
}
} catch {
$errorMessage = $_.Exception.Message
Write-Warning ("Error calculating HumanDmg for {0}: {1}" -f $playerName, $errorMessage)
}
return @{ return @{
playername = $player_name playername = $playerName
humankills = ($kills | where-object { $_.victim.accountId -notlike 'ai.*' }).count humankills = $humanKillsCount
kills = $kills.count kills = $totalKillsCount
deaths = ($deaths).count deaths = $deathsCount # Note: This counts finishes, might differ from match end death reason
gameMode = $gameMode gameMode = $gameMode
matchType = $matchType matchType = $matchType
dbno = ($kills | where-object { $_.dBNOMaker.name -eq $player_name }).count dbno = $dbnoCount
HumanDmg = $HumanDmg HumanDmg = [math]::Round($humanDamageDealt) # Round final value
} }
}
try {
$filesarray = @()
$files = Get-ChildItem -Path "$scriptroot/../data/archive/" -File -ErrorAction Stop
foreach ($file in $files) {
$dateinfile = $file.Name.split('_')[0]
$format = 'yyyy-MM-ddTHH-mm-ss\Z'
$culture = [Globalization.CultureInfo]::InvariantCulture
$dateTime = [datetime]::ParseExact($dateinfile, $format, $culture)
$filesarray += [PSCustomObject]@{name = $file.Name; date = $dateTime }
} }
try { $latestFile = ($filesarray | where-object { ($_.date -gt (get-date).AddDays(-2)) -and ($_.date -lt (get-date).AddDays(-1)) } | Sort-Object date)[0] } # --- Load Old Stats Archive ---
catch { $latestFile = ($filesarray | sort-object date )[-1] } $oldStats = $null
$latestFile = Get-Item -Path "$scriptroot/../data/archive/$($latestFile.name)" $latestArchivePath = $null
Write-Output "Found file $($latestFile.FullName)" try {
$archiveFiles = Get-ChildItem -Path $archiveDir -Filter "*_player_last_stats.json" -File -ErrorAction SilentlyContinue
} if ($archiveFiles) {
catch { # Find the most recent archive file based on filename date
$latestFile = @() $latestArchiveFile = $archiveFiles | Sort-Object -Property Name -Descending | Select-Object -First 1
} $latestArchivePath = $latestArchiveFile.FullName
Write-Output "Attempting to load old stats from: $latestArchivePath"
# Display the result $oldStats = Get-Content -Path $latestArchivePath | ConvertFrom-Json -ErrorAction Stop
if ($latestFile.FullName) { if ($null -eq $oldStats) {
write-host "getting info from $($latestFile.FullName)" Write-Warning "Failed to parse old stats file: $latestArchivePath"
$oldstats = get-content $latestFile.FullName | ConvertFrom-Json } else {
} Write-Output "Successfully loaded old stats."
else { }
write-output 'setting old stats var empty' } else {
$oldstats = @() Write-Output "No old stats archive files found in '$archiveDir'."
} }
} catch {
try { Write-Warning "Error accessing or processing archive directory '$archiveDir': $($_.Exception.Message)"
$all_player_matches = get-content "$scriptroot/../data/player_matches.json" | convertfrom-json -Depth 100 $oldStats = $null # Ensure it's null on error
} }
catch { # Use an empty hashtable if old stats couldn't be loaded, to prevent errors later
Write-Output 'Unable to read file exitin' if ($null -eq $oldStats) {
exit Write-Output "Initializing old stats as empty."
} $oldStats = @{}
}
# --- Load Current Player Matches ---
$allPlayerMatches = $null
if (Test-Path -Path $playerMatchesJsonPath -PathType Leaf) {
try {
$allPlayerMatches = Get-Content -Path $playerMatchesJsonPath | ConvertFrom-Json -Depth 100 -ErrorAction Stop
if ($null -eq $allPlayerMatches -or -not ($allPlayerMatches -is [array])) {
Write-Error "Failed to parse or invalid structure in '$playerMatchesJsonPath'. Cannot proceed."
throw "Invalid player matches data." # Throw to trigger finally block
}
Write-Output "Successfully loaded player matches data. Entry count: $($allPlayerMatches.Count)"
} catch {
Write-Error "Error reading '$playerMatchesJsonPath': $($_.Exception.Message). Cannot proceed."
throw $_ # Re-throw to trigger finally block
}
} else {
Write-Error "Player matches file not found at '$playerMatchesJsonPath'. Cannot proceed."
throw "Missing player matches file." # Throw to trigger finally block
}
foreach ($player in $all_player_matches) { # --- Process Matches & Generate Kill Stats ---
if ($player.psobject.properties.name -eq 'new_win_matches') { Write-Output "Starting match processing and kill stat generation..."
$processedMatchCount = 0
$telemetryDownloads = 0
$telemetryCacheHits = 0
$killStatFilesWritten = 0
# Extract player list (excluding special entries like 'new_win_matches')
$playersToProcess = $allPlayerMatches | Where-Object { $_.PSObject.Properties.Name -ne 'new_win_matches' -and $_.PSObject.Properties.Name -ne 'new_loss_matches' -and $null -ne $_.playername }
foreach ($playerEntry in $playersToProcess) {
$playerName = $playerEntry.playername
if (-not $playerName) {
Write-Warning "Skipping player entry with missing name."
continue continue
} }
$player_name = $player.playername
foreach ($match in $player.player_matches) { if ($null -eq $playerEntry.player_matches -or -not ($playerEntry.player_matches -is [array])) {
write-output "Analyzing match $($match.id) for player $player_name" Write-Warning "Skipping player $playerName due to missing or invalid 'player_matches' array."
if (!(Test-Path -path "$scriptroot/../data/killstats/$($match.id)_$player_name.json" )) { continue
$telemetryfile = "$scriptroot/../data/telemetry_cache/$($match.telemetry_url.split("/")[-1])"
if (!(test-path -Path $telemetryfile)) {
write-output "Saving $telemetryfile"
$telemetry_content = (Invoke-WebRequest -Uri $match.telemetry_url).content
$telemetry_content | out-file $telemetryfile
$telemetry = $telemetry_content | ConvertFrom-Json
}
else {
write-output "Getting from cache $telemetryfile"
$telemetry = get-content $telemetryfile | convertfrom-json
} }
write-output "Analyzing for player $player_name telemetry: $($match.telemetry_url)" foreach ($match in $playerEntry.player_matches) {
$killstat = get-killstats -player_name $player_name -telemetry ($telemetry | where-object { ($_._T -eq 'LOGPLAYERTAKEDAMAGE') -or ($_._T -eq 'LOGPLAYERKILLV2') }) -gameMode $match.gameMode -matchType $match.matchType # Validate essential match data
$matchId = $match.id
$telemetryUrl = $match.telemetry_url
$matchCreatedAt = $match.createdAt
$matchGameMode = $match.gameMode
$matchType = $match.matchType
$matchStats = $match.stats # Player-specific stats within this match entry
if (-not $matchId -or -not $telemetryUrl -or -not $matchCreatedAt -or -not $matchStats) {
Write-Warning "Skipping match for player $playerName due to missing ID, Telemetry URL, Creation Date, or Stats."
continue
}
Write-Verbose "Analyzing match $matchId for player $playerName"
$processedMatchCount++
$killStatFilePath = Join-Path -Path $killStatsPath -ChildPath "${matchId}_${playerName}.json"
if (Test-Path -Path $killStatFilePath -PathType Leaf) {
Write-Verbose "Kill stats file already exists: $killStatFilePath"
continue # Skip if already processed
}
# Get Telemetry Data (Download or Cache)
$telemetry = $null
$telemetryFileName = $telemetryUrl.Split('/')[-1]
$telemetryCacheFilePath = Join-Path -Path $telemetryCachePath -ChildPath $telemetryFileName
if (Test-Path -Path $telemetryCacheFilePath -PathType Leaf) {
Write-Verbose "Loading telemetry from cache: $telemetryCacheFilePath"
try {
$telemetry = Get-Content -Path $telemetryCacheFilePath | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $telemetry) { Write-Warning "Failed to parse cached telemetry: $telemetryCacheFilePath" }
else { $telemetryCacheHits++ }
} catch {
Write-Warning "Error reading cached telemetry '$telemetryCacheFilePath': $($_.Exception.Message)"
}
}
if ($null -eq $telemetry) {
# Download telemetry if not cached or cache read failed
Write-Output ("Downloading telemetry for match {0}: {1}" -f $matchId, $telemetryUrl)
try {
# Add User-Agent
$headers = @{ 'Accept-Encoding' = 'gzip' } # Request compression
$response = Invoke-WebRequest -Uri $telemetryUrl -Headers $headers -UseBasicParsing -ErrorAction Stop
$telemetryContent = $response.Content # Content is automatically decompressed
# Save to cache
$telemetryContent | Out-File -FilePath $telemetryCacheFilePath -Encoding UTF8 -ErrorAction SilentlyContinue
$telemetryDownloads++
# Parse downloaded content
$telemetry = $telemetryContent | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $telemetry) { Write-Warning "Failed to parse downloaded telemetry for match $matchId." }
} catch {
$errorMessage = $_.Exception.Message # Assign to variable first
Write-Warning "Failed to download or save telemetry for match $matchId. URL: $telemetryUrl. Error: $errorMessage" # Use variable in string
# Continue to next match if telemetry fails
continue
}
}
# Process Telemetry if successfully loaded/downloaded
if ($null -ne $telemetry -and ($telemetry -is [array])) {
Write-Verbose "Analyzing telemetry for player $playerName in match $matchId"
# Filter relevant telemetry events once
$relevantTelemetry = $telemetry | Where-Object { $_._T -eq 'LogPlayerTakeDamage' -or $_._T -eq 'LogPlayerKillV2' }
$killStatResult = Get-KillStatsFromTelemetry -playerName $playerName -telemetryEvents $relevantTelemetry -gameMode $matchGameMode -matchType $matchType
if ($null -ne $killStatResult) {
# Find player's win place and death type from the original match data
$playerMatchStats = $allPlayerMatches |
Select-Object -ExpandProperty player_matches |
Where-Object { $_.id -eq $matchId } |
Select-Object -ExpandProperty stats |
Where-Object { $_.name -eq $playerName } |
Select-Object -First 1
$deathType = $playerMatchStats.deathType ?? "unknown"
$winPlace = $playerMatchStats.winPlace ?? 0
$saveKillStats = @{
matchid = $matchId
created = $matchCreatedAt # Use original timestamp
stats = $killStatResult
deathType = $deathType
winplace = $winPlace
}
# Save the individual kill stat file
try {
$saveKillStats | ConvertTo-Json -Depth 5 | Out-File -FilePath $killStatFilePath -Encoding UTF8 -ErrorAction Stop
Write-Output "Written kill stats file: $killStatFilePath"
$killStatFilesWritten++
} catch {
Write-Warning "Failed to write kill stats file '$killStatFilePath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Get-KillStatsFromTelemetry returned null for $playerName in match $matchId."
}
} else {
Write-Warning "Telemetry data for match $matchId is null or not an array after loading/downloading."
}
} # End foreach match
} # End foreach playerEntry
Write-Output "Finished match processing. Processed: $processedMatchCount matches. Telemetry Downloads: $telemetryDownloads, Cache Hits: $telemetryCacheHits. Kill Stats Files Written: $killStatFilesWritten."
# --- Aggregate Kill Stats & Archive Old Files ---
Write-Output "Aggregating kill stats and archiving old files..."
$currentKillStats = @()
$killStatsClanMatchesGt1 = @() # Renamed for clarity
# $killStatsClanMatchesGt2 = @() # Not used later, commented out
# $killStatsClanMatchesGt3 = @() # Not used later, commented out
$archivedKillStatFiles = 0
$processedKillStatFiles = 0
try {
$matchFiles = Get-ChildItem -Path $killStatsPath -File -Filter *.json -ErrorAction SilentlyContinue
if ($null -eq $matchFiles) {
Write-Warning "No kill stat files found in '$killStatsPath'."
} else {
# Determine date threshold for archiving
$archiveThresholdDate = (Get-Date).AddMonths($monthsBack)
Write-Output "Archiving kill stat files older than: $archiveThresholdDate"
# Group files by match ID to count clan participation
$groupedFiles = $matchFiles | Group-Object -Property { $_.Name.Split('_')[0] }
$matchIdsWithClanGt1 = ($groupedFiles | Where-Object { $_.Count -gt 1 }).Name
foreach ($file in $matchFiles) {
$processedKillStatFiles++
try {
$json = Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop
if ($null -eq $json -or $null -eq $json.created -or $null -eq $json.matchid) {
Write-Warning "Skipping invalid or incomplete kill stat file: $($file.Name)"
continue
}
# Attempt to parse the date string
$fileDate = $null
try { $fileDate = [datetime]$json.created } catch { Write-Warning "Could not parse date '$($json.created)' in file $($file.Name)" }
if ($null -ne $fileDate -and $fileDate -ge $archiveThresholdDate) {
# Keep stats if within date range
$currentKillStats += $json
# Add to clan participation list if applicable
if ($matchIdsWithClanGt1 -contains $json.matchid) {
$killStatsClanMatchesGt1 += $json
}
} else {
# Archive old file
Write-Verbose "Archiving $($file.Name)"
Move-Item -Path $file.FullName -Destination $archivePath -Force -ErrorAction SilentlyContinue # Continue if move fails
if ($?) { $archivedKillStatFiles++ }
else { Write-Warning "Failed to archive file: $($file.Name)" }
}
} catch {
Write-Warning "Error processing kill stat file '$($file.Name)': $($_.Exception.Message)"
}
} # End foreach file
Write-Output "Processed $processedKillStatFiles kill stat files. Archived: $archivedKillStatFiles. Kept: $($currentKillStats.Count)."
}
} catch {
Write-Warning "Error reading kill stats directory '$killStatsPath': $($_.Exception.Message)"
}
$savekillstats = @{ # --- Calculate Player Stats per Category ---
matchid = $match.id Write-Output "Calculating aggregated player stats per category..."
created = $match.createdAt
stats = $killstat
deathType = $match.stats.deathType
winplace = (($all_player_matches | where-object { $_.playername -eq $player_name } ).player_matches | where-object { $_.id -eq $match.id }).stats.winplace
}
Write-Output "Writing to file $scriptroot/../data/killstats/$($match.id)_$player_name.json"
$savekillstats | ConvertTo-Json | out-file "$scriptroot/../data/killstats/$($match.id)_$player_name.json"
# Define function locally for clarity, even if slightly redundant
} function Get-AggregatedMatchStatsPlayer {
else {
Write-Output "$($match.id) already in cache"
}
}
}
$killstats = @()
$matchfiles = Get-ChildItem "$scriptroot/../data/killstats/" -File -Filter *.json
$killstats_clan_matches_gt_1 = @()
$killstats_clan_matches_gt_2 = @()
$killstats_clan_matches_gt_3 = @()
$guids = $matchfiles.Name | ForEach-Object { $_.Split("_")[0] }
$groupedGuids_clan_matches_gt_1 = $guids | Group-Object | Where-Object { $_.Count -gt 1 }
$groupedGuids_clan_matches_gt_2 = $guids | Group-Object | Where-Object { $_.Count -gt 2 }
$groupedGuids_clan_matches_gt_3 = $guids | Group-Object | Where-Object { $_.Count -gt 3 }
$last_month = (get-date).AddMonths($monthsback)
foreach ($file in $matchfiles) {
$json = get-content $file | ConvertFrom-Json
if ($json.created -gt $last_month) {
$killstats += $json
if ($groupedGuids_clan_matches_gt_1.Name -contains $json.matchid) {
$killstats_clan_matches_gt_1 += $json
}
if ($groupedGuids_clan_matches_gt_2.Name -contains $json.matchid) {
$killstats_clan_matches_gt_2 += $json
}
if ($groupedGuids_clan_matches_gt_3.Name -contains $json.matchid) {
$killstats_clan_matches_gt_3 += $json
}
}
else {
write-output "Archiving $($file.name)"
Move-Item -Path $file.FullName -Destination "$scriptroot/../data/killstats/archive/." -Force -Verbose
}
}
function Get-MatchStatsPlayer {
param ( param (
[switch] $GameMode, [Parameter(Mandatory=$true)]
[switch] $MatchType, [switch]$FilterByGameMode, # Use specific parameter names
[string] $typemodevalue, [Parameter(Mandatory=$true)]
[array] $playernames, [switch]$FilterByMatchType,
[string] $friendlyname, [Parameter(Mandatory=$true)]
[array] $killstats, [string]$FilterValue, # Value for gameMode or matchType
[string] $sortstat [Parameter(Mandatory=$true)]
[array]$PlayerNames,
[Parameter(Mandatory=$true)]
[string]$CategoryFriendlyName, # Key for looking up old stats
[Parameter(Mandatory=$true)]
[array]$KillStatsToAggregate, # The array of kill stat objects
[Parameter(Mandatory=$true)]
[string]$SortStat,
[Parameter(Mandatory=$true)]
[hashtable]$OldStatsData # Pass old stats explicitly
) )
$MatchStatsPlayer = @()
foreach ($player in $playernames) {
if ($null -eq $player) {
continue
}
if ($GameMode) {
$filterProperty = 'gameMode'
}
if ($MatchType) {
$filterProperty = 'matchType'
}
$alives = (($killstats | where-object { $_.stats.playername -eq $player -and $_.stats.$filterProperty -like $typemodevalue }).deathType | where-object { $_ -eq 'alive' }).count
$deaths = (($killstats | where-object { $_.stats.playername -eq $player -and $_.stats.$filterProperty -like $typemodevalue }).deathType | where-object { $_ -ne 'alive' }).count
$kills = (($killstats.stats | where-object { $_.playername -eq $player -and $_.$filterProperty -like $typemodevalue }).kills | Measure-Object -sum).sum
$dbno = (($killstats.stats | where-object { $_.playername -eq $player -and $_.$filterProperty -like $typemodevalue }).dbno | Measure-Object -sum).sum
$humankills = (($killstats.stats | where-object { $_.playername -eq $player -and $_.$filterProperty -like $typemodevalue }).humankills | Measure-Object -sum).sum
$player_matches = ($killstats.stats | where-object { $_.playername -eq $player -and $_.$filterProperty -like $typemodevalue }).count
$player_wins = ($killstats | where-object { $_.stats.playername -eq $player -and $_.winplace -eq 1 -and $_.stats.$filterProperty -like $typemodevalue }).count
$winratio_old = (($oldstats.$friendlyname | Where-Object { $_.playername -eq $player }).winratio)
$winratio = Get-winratio -player_wins $player_wins -player_matches $player_matches
$change = get-change -OldWinRatio $winratio_old -NewWinRatio $winratio
$avarage_human_damage = [math]::Round((($killstats.stats | where-object { $_.playername -eq $player -and $_.$filterProperty -like $typemodevalue } | Measure-Object -Property HumanDmg -Sum).Sum / $player_matches), 2)
write-host $filterProperty $aggregatedStatsList = @()
write-host $typemodevalue
write-host "Calculating for player $player"
write-host "new winratio $winratio"
write-host "Old winratio $winratio_old"
write-host $change
$MatchStatsPlayer += [PSCustomObject]@{ # Determine the property to filter on based on parameters
alives = $alives $filterProperty = $null
if ($FilterByGameMode) { $filterProperty = 'gameMode' }
elseif ($FilterByMatchType) { $filterProperty = 'matchType' }
else { Write-Error "Get-AggregatedMatchStatsPlayer: Must specify -FilterByGameMode or -FilterByMatchType."; return $null }
foreach ($player in $PlayerNames) {
if (-not $player) { continue } # Skip null/empty player names
# Filter kill stats for the current player and category
$playerKillStats = $KillStatsToAggregate | Where-Object {
$_.stats -ne $null -and
$_.stats.playername -eq $player -and
$_.stats.$filterProperty -like $FilterValue # Use -like for wildcard support if needed (e.g., '*')
}
$playerMatchCount = $playerKillStats.Count
if ($playerMatchCount -eq 0) { continue } # Skip player if no stats in this category
# Aggregate stats using Measure-Object where possible
$totalWins = ($playerKillStats | Where-Object { $_.winplace -eq 1 }).Count
$totalDeaths = ($playerKillStats | Where-Object { $_.deathType -ne 'alive' }).Count # Assuming 'alive' means survived
$totalKills = ($playerKillStats.stats.kills | Measure-Object -Sum -ErrorAction SilentlyContinue).Sum
$totalHumanKills = ($playerKillStats.stats.humankills | Measure-Object -Sum -ErrorAction SilentlyContinue).Sum
$totalDbno = ($playerKillStats.stats.dbno | Measure-Object -Sum -ErrorAction SilentlyContinue).Sum
$totalHumanDmg = ($playerKillStats.stats.HumanDmg | Measure-Object -Sum -ErrorAction SilentlyContinue).Sum
# Calculate Ratios (Handle division by zero)
$kdHuman = if ($totalDeaths -gt 0) { [math]::Round($totalHumanKills / $totalDeaths, 2) } else { $totalHumanKills } # Or "Infinity" string
$kdAll = if ($totalDeaths -gt 0) { [math]::Round($totalKills / $totalDeaths, 2) } else { $totalKills } # Or "Infinity" string
$avgHumanDmg = if ($playerMatchCount -gt 0) { [math]::Round($totalHumanDmg / $playerMatchCount, 2) } else { 0.0 }
# Calculate Win Ratio and Change
$currentWinRatio = Get-WinRatio -playerWins $totalWins -playerMatches $playerMatchCount
$oldWinRatio = $null
# Safely access old stats
if ($OldStatsData.ContainsKey($CategoryFriendlyName)) {
$oldCategoryStats = $OldStatsData[$CategoryFriendlyName]
$playerOldStat = $oldCategoryStats | Where-Object { $_.playername -eq $player } | Select-Object -First 1
if ($playerOldStat -and $playerOldStat.PSObject.Properties.Name -contains 'winratio') {
# Attempt conversion, default to 0.0 if fails
try { $oldWinRatio = [double]$playerOldStat.winratio } catch { $oldWinRatio = 0.0 }
}
}
$winRatioChange = Get-Change -OldWinRatio $oldWinRatio -NewWinRatio $currentWinRatio
Write-Verbose "Stats for $player [$CategoryFriendlyName]: Matches=$playerMatchCount, Wins=$totalWins, Deaths=$totalDeaths, Kills=$totalKills, HKills=$totalHumanKills, AHD=$avgHumanDmg, Win%=$currentWinRatio, Change=$winRatioChange"
$aggregatedStatsList += [PSCustomObject]@{
playername = $player playername = $player
deaths = $deaths deaths = $totalDeaths
kills = $kills kills = $totalKills
humankills = $humankills humankills = $totalHumanKills
matches = $player_matches matches = $playerMatchCount
KD_H = $humankills / $deaths KD_H = $kdHuman
KD_ALL = $kills / $deaths KD_ALL = $kdAll
winratio = $winratio winratio = $currentWinRatio
wins = $player_wins wins = $totalWins
dbno = $dbno dbno = $totalDbno
change = $change change = $winRatioChange # Store the calculated change
ahd = $avarage_human_damage ahd = $avgHumanDmg
}
} # End foreach player
} # Sort the results
} if ($aggregatedStatsList.Count -gt 0 -and $SortStat) {
$MatchStatsPlayer_sorted = $MatchStatsPlayer | ForEach-Object { try {
# Add random key for stable sort behavior if primary sort keys are equal
$aggregatedStatsList = $aggregatedStatsList | ForEach-Object {
$_ | Add-Member -NotePropertyName RandomKey -NotePropertyValue (Get-Random) -PassThru $_ | Add-Member -NotePropertyName RandomKey -NotePropertyValue (Get-Random) -PassThru
} | Sort-Object -Property $sortstat -Descending | Select-Object -Property * -ExcludeProperty RandomKey #randomize the order } | Sort-Object -Property $SortStat, RandomKey -Descending | Select-Object -Property * -ExcludeProperty RandomKey
} catch {
Write-Warning "Failed to sort aggregated stats by '$SortStat'. Error: $($_.Exception.Message)"
}
}
return $MatchStatsPlayer_sorted return $aggregatedStatsList
} } # End function Get-AggregatedMatchStatsPlayer
# Get list of unique player names from the loaded match data
$uniquePlayerNames = ($playersToProcess.playername | Select-Object -Unique)
# Calculate stats for each category
$playerStatsEventIbr = Get-AggregatedMatchStatsPlayer -FilterByGameMode -FilterValue 'ibr' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'Intense' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsAiRoyale = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue 'airoyale' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'Casual' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsOfficial = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue 'official' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'official' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsCustom = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue 'custom' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'custom' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsAll = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue '*' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'all' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsRanked = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue 'competitive' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'Ranked' -KillStatsToAggregate $currentKillStats -SortStat 'ahd' -OldStatsData $oldStats
$playerStatsAiRoyaleClanGt1 = Get-AggregatedMatchStatsPlayer -FilterByMatchType -FilterValue 'airoyale' -PlayerNames $uniquePlayerNames -CategoryFriendlyName 'Casual' -KillStatsToAggregate $killStatsClanMatchesGt1 -SortStat 'ahd' -OldStatsData $oldStats # Use filtered killstats
# Apply specific sorting if needed (e.g., custom by winratio)
if ($playerStatsCustom) {
$playerStatsCustom = $playerStatsCustom | Sort-Object winratio -Descending
}
$playerstats_event_ibr = Get-MatchStatsPlayer -GameMode -typemodevalue 'ibr' -playernames $all_player_matches.playername -friendlyname 'Intense' -killstats $killstats -sortstat 'ahd' # --- Save Aggregated Stats ---
$playerstats_airoyale = Get-MatchStatsPlayer -MatchType -typemodevalue 'airoyale' -playernames $all_player_matches.playername -friendlyname 'Casual' -killstats $killstats -sortstat 'ahd' Write-Output "Saving aggregated player stats..."
$playerstats_official = Get-MatchStatsPlayer -MatchType -typemodevalue 'official' -playernames $all_player_matches.playername -friendlyname 'official' -killstats $killstats -sortstat 'ahd' $currentDateTime = Get-Date
$playerstats_custom = Get-MatchStatsPlayer -MatchType -typemodevalue 'custom' -playernames $all_player_matches.playername -friendlyname 'custom' -killstats $killstats -sortstat 'ahd' $currentTimezone = (Get-TimeZone).Id
$playerstats_all = Get-MatchStatsPlayer -MatchType -typemodevalue '*' -playernames $all_player_matches.playername -friendlyname 'all' -killstats $killstats -sortstat 'ahd' $formattedString = "$currentDateTime - Time Zone: $currentTimezone"
$playerstats_ranked = Get-MatchStatsPlayer -MatchType -typemodevalue 'competitive' -playernames $all_player_matches.playername -friendlyname 'Ranked' -killstats $killstats -sortstat 'ahd'
$playerstats_airoyale_clan_gt_1 = Get-MatchStatsPlayer -MatchType -typemodevalue 'airoyale' -playernames $all_player_matches.playername -friendlyname 'Casual' -killstats $killstats_clan_matches_gt_1 -sortstat 'ahd' $playerStatsOutput = [PSCustomObject]@{
all = $playerStatsAll
$playerstats_custom = $playerstats_custom | Sort-Object winratio -Descending clan_casual = $playerStatsAiRoyaleClanGt1
Intense = $playerStatsEventIbr
$currentDateTime = Get-Date Casual = $playerStatsAiRoyale
official = $playerStatsOfficial
# Get current timezone custom = $playerStatsCustom
$currentTimezone = (Get-TimeZone).Id
# Format and parse the information into a string
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
# Output the formatted string
$playerstats = [PSCustomObject]@{
all = $playerstats_all
clan_casual = $playerstats_airoyale_clan_gt_1
Intense = $playerstats_event_ibr
Casual = $playerstats_airoyale
official = $playerstats_official
custom = $playerstats_custom
updated = $formattedString updated = $formattedString
Ranked = $playerstats_ranked Ranked = $playerStatsRanked
}
# Save to current stats file
try {
$playerStatsOutput | ConvertTo-Json -Depth 10 | Out-File -FilePath $lastStatsJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Aggregated stats saved to '$lastStatsJsonPath'"
} catch {
Write-Warning "Failed to save aggregated stats to '$lastStatsJsonPath'. Error: $($_.Exception.Message)"
}
# Save to archive file
$archiveFileNameDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH-mm-ssZ") -replace ":", "-"
$archiveFilePath = Join-Path -Path $archiveDir -ChildPath "${archiveFileNameDate}_player_last_stats.json"
try {
Write-Output "Archiving aggregated stats to: $archiveFilePath"
$playerStatsOutput | ConvertTo-Json -Depth 10 | Out-File -FilePath $archiveFilePath -Encoding UTF8 -ErrorAction Stop
} catch {
Write-Warning "Failed to archive aggregated stats to '$archiveFilePath'. Error: $($_.Exception.Message)"
}
# --- Clean Telemetry Cache ---
Write-Output "Cleaning telemetry cache..."
try {
# Get telemetry URLs from the *currently loaded* player matches data
$activeTelemetryUrls = ($allPlayerMatches | Select-Object -ExpandProperty player_matches).telemetry_url | Select-Object -Unique
$filesToKeep = $activeTelemetryUrls | ForEach-Object { if ($_) { $_.Split('/')[-1] } } # Get just the filenames
$cachedFiles = Get-ChildItem -Path $telemetryCachePath -File -ErrorAction SilentlyContinue
if ($cachedFiles) {
$filesToRemove = Compare-Object -ReferenceObject $filesToKeep -DifferenceObject $cachedFiles.Name -PassThru | Where-Object { $_ -ne $null }
$removedCount = 0
foreach ($fileToRemoveName in $filesToRemove) {
$fileToRemovePath = Join-Path -Path $telemetryCachePath -ChildPath $fileToRemoveName
Write-Verbose "Removing cached telemetry file: $fileToRemovePath"
Remove-Item -Path $fileToRemovePath -Force -ErrorAction SilentlyContinue
if ($?) { $removedCount++ }
else { Write-Warning "Failed to remove cache file: $fileToRemovePath" }
}
Write-Output "Telemetry cache cleanup complete. Removed $removedCount files."
} else {
Write-Output "Telemetry cache directory is empty or inaccessible."
}
} catch {
Write-Warning "Error during telemetry cache cleanup: $($_.Exception.Message)"
}
Write-Output "Match parsing and aggregation complete."
} # End Main Try Block
finally {
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock # Ensure lock is always removed
Stop-Transcript
} }
write-output "Writing file"
($playerstats | convertto-json) | out-file "$scriptroot/../data/player_last_stats.json"
$date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$filenameDate = ($date -replace ":", "-")
write-output "writing to file : $scriptroot/../data/archive/$($filenameDate)_player_last_stats.json"
($playerstats | convertto-json) | out-file "$scriptroot/../data/archive/$($filenameDate)_player_last_stats.json"
write-output "Cleaning cache"
$files_keep = (($all_player_matches).player_matches.telemetry_url | Select-Object -Unique) | ForEach-Object { $_.split("/")[-1] }
$files_cache = (get-childitem "$scriptroot/../data/telemetry_cache/").name
$difference = (Compare-Object -ReferenceObject $files_keep -DifferenceObject $files_cache | Where-Object { $_.SideIndicator -eq "=>" }).InputObject
foreach ($file in $difference) {
write-output "removing $scriptroot/../data/telemetry_cache/$file"
Remove-Item -Path "$scriptroot/../data/telemetry_cache/$file"
}
write-output "Operation complete"
remove-lock
Stop-Transcript

View file

@ -1,50 +1,178 @@
if($PSScriptRoot.length -eq 0){ # --- Script Setup ---
$scriptroot = Get-Location # Using Unicode BOM (Byte Order Mark) can sometimes cause issues, ensure file is saved as UTF-8 without BOM if problems arise.
}else{ Start-Transcript -Path '/var/log/dtch/update_clan.log' -Append
$scriptroot = $PSScriptRoot Write-Output "Starting update_clan script at $(Get-Date)"
} Write-Output "Running from: $(Get-Location)"
. $scriptroot\..\includes\ps1\lockfile.ps1
new-lock -by "update_clan"
# Read the content of the file as a single string
$fileContent = Get-Content -Path "$scriptroot/../config/config.php" -Raw
# Use regex to match the apiKey value # Determine script root directory reliably
if ($fileContent -match "\`$apiKey\s*=\s*\'([^\']+)\'") { if ($PSScriptRoot) {
$apiKey = $matches[1] $scriptRoot = $PSScriptRoot
}else {
Write-Output "API Key not found"
}
if ($fileContent -match "\`$clanid\s*=\s*\'([^\']+)\'") {
$clanid = $matches[1]
} else { } else {
Write-Output "No clanid found in $configPath" # Fallback for environments where $PSScriptRoot is not defined (e.g., ISE)
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
Write-Output "Script root identified as: $scriptRoot"
# Define paths using Join-Path for robustness
$includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
# Ensure data directory exists (copied from update_clan_members.ps1 for consistency)
if (-not (Test-Path -Path $dataPath -PathType Container)) {
Write-Warning "Data directory not found at '$dataPath'. Attempting to create."
try {
New-Item -Path $dataPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-Output "Successfully created data directory."
} catch {
Write-Error "Failed to create data directory '$dataPath'. Please check permissions. Error: $($_.Exception.Message)"
Stop-Transcript
exit 1
}
}
# --- Locking ---
$lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
exit 1
}
. $lockFilePath
New-Lock -by "update_clan" -ErrorAction Stop # Stop if locking fails
# --- Configuration Loading ---
$apiKey = $null
$clanId = $null
# Load API Key and Clan ID from config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
# Corrected regex for apiKey
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') {
$apiKey = $matches[1]
Write-Output "API Key loaded successfully."
} else {
Write-Warning "API Key pattern not found in '$phpConfigPath'."
}
# Corrected regex for clanid
if ($fileContent -match '^\s*\$clanid\s*=\s*''([^'']+)''') {
$clanId = $matches[1]
Write-Output "Clan ID loaded successfully."
} else {
Write-Warning "Clan ID pattern not found in '$phpConfigPath'."
}
} catch {
Write-Warning "Failed to read '$phpConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Config file not found at '$phpConfigPath'."
}
# Validate required config
if (-not $apiKey) {
Write-Error "API Key could not be loaded. Cannot proceed."
Remove-Lock
Stop-Transcript
exit 1
}
if (-not $clanId) {
Write-Error "Clan ID could not be loaded. Cannot proceed."
Remove-Lock
Stop-Transcript
exit 1
}
# --- Helper Function for API Calls (Copied from update_clan_members.ps1 for self-containment) ---
function Invoke-PubgApi {
param(
[Parameter(Mandatory=$true)]
[string]$Uri,
[Parameter(Mandatory=$true)]
[hashtable]$Headers,
[int]$RetryCount = 1,
[int]$RetryDelaySeconds = 61
)
for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
try {
Write-Verbose "Attempting API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
if ($null -ne $response) {
Write-Verbose "API call successful."
return $response
} else {
Write-Warning "API call to $Uri returned null or empty response."
return $null
}
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping for $RetryDelaySeconds seconds before retry..."
Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) {
Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"
return $null
} else {
Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"
return $null
}
}
}
return $null
}
# --- Get Clan Information ---
Write-Output "Fetching clan information for ID: $clanId"
$headers = @{ $headers = @{
'accept' = 'application/vnd.api+json' 'accept' = 'application/vnd.api+json'
'Authorization' = "$apiKey" 'Authorization' = "Bearer $apiKey" # Standard practice
} }
try { $apiUrl = "https://api.pubg.com/shards/steam/clans/$clanId"
$claninfo = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/clans/$clanid" -Method GET -Headers $headers
} catch { $clanInfoResponse = Invoke-PubgApi -Uri $apiUrl -Headers $headers
write-output "sleeping for 61 sec"
start-sleep -Seconds 61 # --- Process and Save Clan Data ---
$claninfo = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/clans/$clanid" -Method GET -Headers $headers if ($null -ne $clanInfoResponse -and $null -ne $clanInfoResponse.data.attributes) {
Write-Output "Successfully retrieved clan information."
# Create PS Custom Object from attributes
$clanData = [PSCustomObject]$clanInfoResponse.data.attributes
# Add update timestamp
$currentDateTime = Get-Date
$currentTimezone = (Get-TimeZone).Id
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
$clanData | Add-Member -Name "updated" -MemberType NoteProperty -Value $formattedString
Write-Output "Added update timestamp: $formattedString"
# Save clan data to JSON file
$clanInfoJsonPath = Join-Path -Path $dataPath -ChildPath "claninfo.json"
try {
$clanData | ConvertTo-Json -Depth 100 | Out-File -FilePath $clanInfoJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Clan info saved to '$clanInfoJsonPath'"
# Output JSON to transcript for verification (optional)
# Write-Output "Saved Data:"
# $clanData | ConvertTo-Json -Depth 100 | Write-Output
} catch {
Write-Error "Failed to save clan info to '$clanInfoJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Error "Failed to retrieve valid clan information from API for ID: $clanId"
# Consider if script should exit or continue
} }
# Get current date and time
$currentDateTime = Get-Date
# Get current timezone # --- Cleanup ---
$currentTimezone = (Get-TimeZone).Id Write-Output "Script finished at $(Get-Date)."
Remove-Lock
# Format and parse the information into a string Stop-Transcript
$formattedString = "$currentDateTime - Time Zone: $currentTimezone"
# Output the formatted string
[PSCustomObject]$clandata = $claninfo.data.attributes
$clandata | Add-Member -Name "updated" -MemberType NoteProperty -Value $formattedString
$clandata | convertto-json -Depth 100 | out-file "$scriptroot/../data/claninfo.json"
$clandata | convertto-json -Depth 100
remove-lock

View file

@ -1,202 +1,335 @@
Start-Transcript -Path '/var/log/dtch/update_clan_members.log' -Append # --- Script Setup ---
Write-Output 'Running from' # Using Unicode BOM (Byte Order Mark) can sometimes cause issues, ensure file is saved as UTF-8 without BOM if problems arise.
(Get-Location).path Start-Transcript -Path '/var/log/dtch/update_clan_members.log' -Append
Write-Output "Starting update_clan_members script at $(Get-Date)"
Write-Output "Running from: $(Get-Location)"
if ($PSScriptRoot.length -eq 0) { # Determine script root directory reliably
$scriptroot = Get-Location if ($PSScriptRoot) {
} $scriptRoot = $PSScriptRoot
else { } else {
$scriptroot = $PSScriptRoot # Fallback for environments where $PSScriptRoot is not defined (e.g., ISE)
$scriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Write-Warning "PSScriptRoot not defined, using calculated path: $scriptRoot"
} }
Write-Output "Script root identified as: $scriptRoot"
. $scriptroot\..\includes\ps1\lockfile.ps1 # Define paths using Join-Path for robustness
new-lock -by "update_clan_members" $includesPath = Join-Path -Path $scriptRoot -ChildPath "..\includes\ps1"
$configPath = Join-Path -Path $scriptRoot -ChildPath "..\config"
$dataPath = Join-Path -Path $scriptRoot -ChildPath "..\data"
# Read the content of the file as a single string # Ensure data directory exists
$fileContent = Get-Content -Path "$scriptroot/../config/config.php" -Raw if (-not (Test-Path -Path $dataPath -PathType Container)) {
Write-Warning "Data directory not found at '$dataPath'. Attempting to create."
# Use regex to match the apiKey value try {
if ($fileContent -match "\`$apiKey\s*=\s*\'([^\']+)\'") { New-Item -Path $dataPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
$apiKey = $matches[1] Write-Output "Successfully created data directory."
} } catch {
else { Write-Error "Failed to create data directory '$dataPath'. Please check permissions. Error: $($_.Exception.Message)"
Write-Output "API Key not found" Stop-Transcript
} exit 1
$clanMembersArray = (Get-Content "$scriptroot/../config/clanmembers.json" | ConvertFrom-Json).clanMembers
$clanmemberchunks = @()
$chunk = @()
$chunksize = 10
$i = 0
foreach ($member in $clanMembersArray) {
$chunk += $member
if ($chunk.Count -eq $chunksize) {
$clanmemberchunks += @{ "Chunk$i" = $chunk }
$chunk = @()
$i++
} }
} }
# Add any remaining members to the last chunk # --- Locking ---
if ($chunk.Count -gt 0) { $lockFilePath = Join-Path -Path $includesPath -ChildPath "lockfile.ps1"
$clanmemberchunks += @{ "Chunk$i" = $chunk } if (-not (Test-Path -Path $lockFilePath -PathType Leaf)) {
Write-Error "Lockfile script not found at '$lockFilePath'. Cannot proceed."
Stop-Transcript
exit 1
} }
$clanMembers = $clanMembersArray -join ',' . $lockFilePath
New-Lock -by "update_clan_members" -ErrorAction Stop # Stop if locking fails
# --- Configuration Loading ---
$apiKey = $null
$clanMembersArray = @()
# Load API Key from config.php
$phpConfigPath = Join-Path -Path $configPath -ChildPath "config.php"
if (Test-Path -Path $phpConfigPath -PathType Leaf) {
try {
$fileContent = Get-Content -Path $phpConfigPath -Raw -ErrorAction Stop
# Corrected regex: Match literal '$apiKey', whitespace, '=', whitespace, single quote, capture content, single quote.
if ($fileContent -match '^\s*\$apiKey\s*=\s*''([^'']+)''') {
$apiKey = $matches[1]
Write-Output "API Key loaded successfully."
} else {
Write-Warning "API Key pattern not found in '$phpConfigPath'."
}
} catch {
Write-Warning "Failed to read '$phpConfigPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Config file not found at '$phpConfigPath'."
}
if (-not $apiKey) {
Write-Error "API Key could not be loaded. Cannot proceed without API Key."
Remove-Lock
Stop-Transcript
exit 1
}
# Load Clan Members from clanmembers.json
$clanMembersJsonPath = Join-Path -Path $configPath -ChildPath "clanmembers.json"
if (Test-Path -Path $clanMembersJsonPath -PathType Leaf) {
try {
$clanMembersData = Get-Content -Path $clanMembersJsonPath -Raw | ConvertFrom-Json -ErrorAction Stop
if ($clanMembersData -is [PSCustomObject] -and $clanMembersData.PSObject.Properties.Name -contains 'clanMembers' -and $clanMembersData.clanMembers -is [array]) {
$clanMembersArray = $clanMembersData.clanMembers
Write-Output "Clan members loaded successfully. Count: $($clanMembersArray.Count)"
} else {
Write-Warning "Invalid structure in '$clanMembersJsonPath'. Expected an object with a 'clanMembers' array."
}
} catch {
Write-Warning "Failed to read or parse '$clanMembersJsonPath'. Error: $($_.Exception.Message)"
}
} else {
Write-Warning "Clan members file not found at '$clanMembersJsonPath'."
}
if ($clanMembersArray.Count -eq 0) {
Write-Error "No clan members loaded. Cannot proceed."
Remove-Lock
Stop-Transcript
exit 1
}
# --- Helper Function for API Calls ---
function Invoke-PubgApi {
param(
[Parameter(Mandatory=$true)]
[string]$Uri,
[Parameter(Mandatory=$true)]
[hashtable]$Headers,
[int]$RetryCount = 1,
[int]$RetryDelaySeconds = 61
)
for ($attempt = 1; $attempt -le ($RetryCount + 1); $attempt++) {
try {
Write-Verbose "Attempting API call (Attempt $($attempt)): $Uri"
$response = Invoke-RestMethod -Uri $Uri -Method GET -Headers $Headers -ErrorAction Stop
# Basic validation: Check if response is not null
if ($null -ne $response) {
Write-Verbose "API call successful."
return $response
} else {
Write-Warning "API call to $Uri returned null or empty response."
# Decide if null response is an error or expected empty result
return $null # Or handle as error if appropriate
}
} catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $($attempt)). Status: $statusCode. Error: $errorMessage"
# Check for rate limit (429) or other retryable errors if needed
if ($attempt -le $RetryCount -and $statusCode -eq 429) {
Write-Warning "Rate limit hit. Sleeping for $RetryDelaySeconds seconds before retry..."
Start-Sleep -Seconds $RetryDelaySeconds
} elseif ($attempt -gt $RetryCount) {
Write-Error "API call failed after $($attempt) attempts. URI: $Uri. Last Error: $errorMessage"
# Re-throw the exception or return null/specific error object
# throw $_ # Re-throw the last exception to halt script if critical
return $null # Return null to allow script to potentially continue or handle missing data
} else {
# Handle other non-retryable errors immediately
Write-Error "Non-retryable API error. URI: $Uri. Error: $errorMessage"
return $null
}
}
}
# Should not be reached if logic is correct, but return null just in case
return $null
}
# --- Get Player Information ---
Write-Output "Fetching player information..."
$headers = @{ $headers = @{
'accept' = 'application/vnd.api+json' 'accept' = 'application/vnd.api+json'
'Authorization' = "$apiKey" 'Authorization' = "Bearer $apiKey" # Standard practice to include "Bearer"
} }
# Chunk clan members for API query (max 10 per request)
$playerinfo = @() $clanMemberChunks = @()
foreach ($key in $clanmemberchunks.keys) { $chunkSize = 10
for ($i = 0; $i -lt $clanMembersArray.Count; $i += $chunkSize) {
$clanMembers = $clanmemberchunks.$key -join ',' $endIndex = [System.Math]::Min($i + $chunkSize - 1, $clanMembersArray.Count - 1)
$clanMembers $clanMemberChunks += ,($clanMembersArray[$i..$endIndex]) # Use comma to ensure it's always an array of arrays
try {
$playerinfo += Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/players?filter[playerNames]=$clanMembers" -Method GET -Headers $headers
}
catch {
write-output 'Sleeping for 61 seconds'
start-sleep -Seconds 61
$playerinfo += Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/players?filter[playerNames]=$clanMembers" -Method GET -Headers $headers
}
} }
$playerinfo.data | convertto-json -depth 100 | Out-File "$scriptroot/../data/player_data.json" Write-Output "Split clan members into $($clanMemberChunks.Count) chunks."
$allPlayerInfoData = @()
foreach ($chunk in $clanMemberChunks) {
$playerNamesParam = $chunk -join ','
$apiUrl = "https://api.pubg.com/shards/steam/players?filter[playerNames]=$playerNamesParam"
Write-Output "Querying for players: $playerNamesParam"
$playerInfoResponse = Invoke-PubgApi -Uri $apiUrl -Headers $headers
if ($null -ne $playerInfoResponse -and $playerInfoResponse.data -is [array]) {
$allPlayerInfoData += $playerInfoResponse.data
Write-Output "Received data for $($playerInfoResponse.data.Count) players in this chunk."
} else {
Write-Warning "No valid player data received for chunk: $playerNamesParam"
# Consider if script should stop or continue if a chunk fails
}
}
# Process player info if data was retrieved
$playerList = @() $playerList = @()
$playerinfo.data | ForEach-Object { if ($allPlayerInfoData.Count -gt 0) {
$playerObject = [PSCustomObject]@{ # Save raw player data
$playerDataJsonPath = Join-Path -Path $dataPath -ChildPath "player_data.json"
try {
@{ data = $allPlayerInfoData } | ConvertTo-Json -Depth 100 | Out-File -FilePath $playerDataJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Player data saved to '$playerDataJsonPath'"
} catch {
Write-Warning "Failed to save player data to '$playerDataJsonPath'. Error: $($_.Exception.Message)"
}
# Create simplified player list (Name/ID mapping)
$playerList = $allPlayerInfoData | ForEach-Object {
if ($_.attributes -ne $null -and $_.id -ne $null) {
[PSCustomObject]@{
PlayerName = $_.attributes.name PlayerName = $_.attributes.name
PlayerID = $_.id PlayerID = $_.id
} }
$playerList += $playerObject } else {
Write-Warning "Skipping player entry due to missing attributes or ID: $($_.PSObject.Properties | Out-String)"
}
}
Write-Output "Processed $($playerList.Count) players into PlayerList."
# $playerList | Format-Table # Optional: Display the list
} else {
Write-Error "No player information retrieved from API. Cannot proceed with stats fetching."
Remove-Lock
Stop-Transcript
exit 1
} }
# Display the list # --- Get Lifetime Stats ---
$playerList Write-Output "Fetching lifetime stats..."
$playerModes = @("solo", "duo", "squad", "solo-fpp", "duo-fpp", "squad-fpp")
$lifetimeStats = @{} # Use hashtable for structured storage: $lifetimeStats[mode][playerName][accountId] = stats
# Chunk player IDs for API query (max 10 per request)
$playerIdChunks = @()
for ($i = 0; $i -lt $playerList.Count; $i += $chunkSize) {
$endIndex = [System.Math]::Min($i + $chunkSize - 1, $playerList.Count - 1)
$playerIdChunks += ,($playerList[$i..$endIndex].PlayerID)
}
Write-Output "Split player IDs into $($playerIdChunks.Count) chunks."
foreach ($idChunk in $playerIdChunks) {
$playerIdsParam = $idChunk -join ','
foreach ($playMode in $playerModes) {
Write-Output "Getting lifetime stats for mode '$playMode', players: $playerIdsParam"
$apiUrl = "https://api.pubg.com/shards/steam/seasons/lifetime/gameMode/$playMode/players?filter[playerIds]=$playerIdsParam"
$playerChunks = @{} $statsResponse = Invoke-PubgApi -Uri $apiUrl -Headers $headers
$chunk = @()
$chunksize = 10
$i = 0
foreach ($player in $playerList) { if ($null -ne $statsResponse -and $statsResponse.data -is [array]) {
$chunkName = "Chunk$i" Write-Verbose "Received $($statsResponse.data.Count) stat entries for mode '$playMode'."
$chunk += $player # Initialize mode in hashtable if it doesn't exist
if ($chunk.Count -eq $chunksize) { if (-not $lifetimeStats.ContainsKey($playMode)) {
$playerChunks[$chunkName] = $chunk $lifetimeStats[$playMode] = @{}
$chunk = @() }
$i++
# Process each stat entry in the response
foreach ($statEntry in $statsResponse.data) {
# Validate structure before accessing nested properties
if ($null -ne $statEntry.relationships.player.data.id -and $null -ne $statEntry.attributes.gameModeStats.$playMode) {
$accountId = $statEntry.relationships.player.data.id
$specificStat = $statEntry.attributes.gameModeStats.$playMode
# Find player name from our $playerList
$playerName = ($playerList | Where-Object { $_.PlayerID -eq $accountId } | Select-Object -First 1).PlayerName
if ($playerName) {
# Initialize player in hashtable if it doesn't exist
if (-not $lifetimeStats[$playMode].ContainsKey($playerName)) {
$lifetimeStats[$playMode][$playerName] = @{}
}
# Store the stats under the account ID for that player/mode
$lifetimeStats[$playMode][$playerName][$accountId] = $specificStat
Write-Verbose "Stored stats for $playerName ($accountId) in mode $playMode."
} else {
Write-Warning "Could not find player name for account ID '$accountId' in PlayerList."
}
} else {
Write-Warning "Skipping stat entry due to missing data/relationships/gameModeStats: $($statEntry | Out-String)"
}
}
} else {
Write-Warning "No valid lifetime stats data received for mode '$playMode', players: $playerIdsParam"
}
} }
} }
# Add any remaining players to the last chunk # Add update timestamp and save lifetime stats
if ($chunk.Count -gt 0) {
$playerChunks["Chunk$i"] = $chunk
}
$playeridstringarray = @()
foreach ($key in $playerChunks.keys) {
$playeridstringarray += $playerChunks.$key.PlayerID -join ','
}
$playermodes = @(
"solo",
"duo",
"squad",
"solo-fpp",
"duo-fpp",
"squad-fpp"
)
# Initialize the master hashtable
$lifetimestats = @{}
foreach ($playeridstring in $playeridstringarray) {
foreach ($playmode in $playermodes) {
# Fetch stats for the current playmode
write-output "Getting data for players $playeridstring gameode $playmode"
try {
$stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/seasons/lifetime/gameMode/$playmode/players?filter[playerIds]=$playeridstring" -Method GET -Headers $headers
}
catch {
write-output 'sleeping for 61 seconds'
start-sleep -Seconds 61
$stats = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/seasons/lifetime/gameMode/$playmode/players?filter[playerIds]=$playeridstring" -Method GET -Headers $headers
}
# Check if the playmode doesn't exist in the hashtable, then add it
if (-not $lifetimestats.ContainsKey($playmode)) {
$lifetimestats[$playmode] = @{}
}
foreach ($stat in $stats.data.relationships.player.data.id) {
# Fetch the player name for the current stat (account ID) from the dictionary
$playerName = $playerList | Where-Object { $_.PlayerID -eq $stat } | Select-Object -ExpandProperty PlayerName
write-output "Getting data for $playerName with gamemode $playmode"
# Fetch the specific stat data for the current stat
$specificStat = ($stats.data | where-object { $_.relationships.player.data.id -eq $stat }).attributes.gamemodestats.$playmode
# Create a new hashtable entry for the player and insert the specific stat data
if (-not $lifetimestats[$playmode].ContainsKey($playerName)) {
$lifetimestats[$playmode][$playerName] = @{}
}
$lifetimestats[$playmode][$playerName][$stat] = $specificStat
}
}
}
# Get current date and time
$currentDateTime = Get-Date $currentDateTime = Get-Date
# Get current timezone
$currentTimezone = (Get-TimeZone).Id $currentTimezone = (Get-TimeZone).Id
# Format and parse the information into a string
$formattedString = "$currentDateTime - Time Zone: $currentTimezone" $formattedString = "$currentDateTime - Time Zone: $currentTimezone"
$lifetimestats['updated'] = $formattedString $lifetimeStats['updated'] = $formattedString
# Output the formatted string Write-Output "Added update timestamp: $formattedString"
$lifetimestats | convertto-json -Depth 100 | out-file "$scriptroot/../data/player_lifetime_data.json"
$seasons = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/seasons" -Method GET -Headers $headers
$current_season = $seasons.data | Where-Object {$_.attributes.isCurrentSeason -eq $true}
$i = 0
$seasonstats = @()
while($playerinfo.data.Count -gt $i) {
write-host $clanMembersArray[$i]
try{
$rankedstat = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/players/$($playerinfo.data[$i].id)/seasons/$($current_season.id)/ranked" -Method GET -Headers $headers
}catch{
write-output 'sleeping for 61 seconds'
start-sleep -Seconds 61
$rankedstat = Invoke-RestMethod -Uri "https://api.pubg.com/shards/steam/players/$($playerinfo.data[$i].id)/seasons/$($current_season.id)/ranked" -Method GET -Headers $headers
}
$seasonstats += [PSCustomObject]@{
stat = $rankedstat
name = $playerinfo.data[$i].attributes.name
}
$i++
$lifetimeStatsJsonPath = Join-Path -Path $dataPath -ChildPath "player_lifetime_data.json"
try {
$lifetimeStats | ConvertTo-Json -Depth 100 | Out-File -FilePath $lifetimeStatsJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Lifetime stats saved to '$lifetimeStatsJsonPath'"
} catch {
Write-Warning "Failed to save lifetime stats to '$lifetimeStatsJsonPath'. Error: $($_.Exception.Message)"
} }
$seasonstats | Sort-Object -Property {$_.stat.data.attributes.rankedGameModeStats.'squad-fpp'.currentRankPoint} -Descending | convertto-json -Depth 100| Out-File "$scriptroot/../data/player_season_data.json"
remove-lock # --- Get Current Season Ranked Stats ---
Write-Output "Fetching current season information..."
$currentSeason = $null
$seasonsResponse = Invoke-PubgApi -Uri "https://api.pubg.com/shards/steam/seasons" -Headers $headers
if ($null -ne $seasonsResponse -and $seasonsResponse.data -is [array]) {
$currentSeason = $seasonsResponse.data | Where-Object { $_.attributes.isCurrentSeason -eq $true } | Select-Object -First 1
}
if (-not $currentSeason) {
Write-Warning "Could not determine the current season from API. Skipping ranked stats update."
} else {
Write-Output "Current season identified: $($currentSeason.id)"
$seasonStats = @()
# Iterate through the validated $playerList
foreach ($player in $playerList) {
Write-Output "Getting ranked stats for player: $($player.PlayerName) ($($player.PlayerID))"
$apiUrl = "https://api.pubg.com/shards/steam/players/$($player.PlayerID)/seasons/$($currentSeason.id)/ranked"
$rankedStatResponse = Invoke-PubgApi -Uri $apiUrl -Headers $headers
# Even if API call returns null (e.g., player has no ranked stats), store an entry
$seasonStats += [PSCustomObject]@{
stat = $rankedStatResponse # Store the whole response (or null)
name = $player.PlayerName
}
Write-Verbose "Stored ranked stat entry for $($player.PlayerName)."
}
# Save season stats (sorting might fail if 'stat' or nested properties are null)
$seasonStatsJsonPath = Join-Path -Path $dataPath -ChildPath "player_season_data.json"
try {
# Sort carefully, handling potential nulls
$sortedSeasonStats = $seasonStats | Sort-Object -Property { if ($null -ne $_.stat.data.attributes.rankedGameModeStats.'squad-fpp'.currentRankPoint) { $_.stat.data.attributes.rankedGameModeStats.'squad-fpp'.currentRankPoint } else { 0 } } -Descending
$sortedSeasonStats | ConvertTo-Json -Depth 100 | Out-File -FilePath $seasonStatsJsonPath -Encoding UTF8 -ErrorAction Stop
Write-Output "Season stats saved to '$seasonStatsJsonPath'"
} catch {
Write-Warning "Failed to save season stats to '$seasonStatsJsonPath'. Sorting or file write failed. Error: $($_.Exception.Message)"
}
}
# --- Cleanup ---
Write-Output "Script finished at $(Get-Date)."
Remove-Lock
Stop-Transcript Stop-Transcript

View file

@ -1,10 +1,75 @@
<?php <?php
// --- Configuration and Data Fetching ---
$ogDescription = "Explore detailed lifetime statistics of PUBG players in various game modes including solo, duo, and squad. Choose your favorite mode and player to view their performance stats, victories, and more, updated regularly."; $ogDescription = "Explore detailed lifetime statistics of PUBG players in various game modes including solo, duo, and squad. Choose your favorite mode and player to view their performance stats, victories, and more, updated regularly.";
?>
// Include config if it exists
$configPath = './config/config.php';
if (file_exists($configPath)) {
include $configPath;
} else {
// Handle missing config file, maybe set defaults or show an error
// For now, we'll proceed assuming defaults or that it's not strictly required for this page structure
}
$players_data = null;
$stats = null;
$selected_player = null;
$dataError = '';
$lastUpdated = 'N/A';
$availableModes = ['solo', 'duo', 'squad', 'solo-fpp', 'duo-fpp', 'squad-fpp']; // Define available modes
// Determine selected game mode
$selected_mode = isset($_GET['game_mode']) && in_array($_GET['game_mode'], $availableModes) ? $_GET['game_mode'] : 'squad';
// Load player lifetime data
$dataPath = './data/player_lifetime_data.json';
if (file_exists($dataPath)) {
$jsonData = file_get_contents($dataPath);
$players_data = json_decode($jsonData, true);
if (!is_array($players_data)) {
$dataError = "Error decoding player lifetime data.";
$players_data = null; // Ensure it's null if decoding failed
} else {
$lastUpdated = htmlspecialchars($players_data['updated'] ?? 'N/A');
// Check if the selected mode exists in the data
if (isset($players_data[$selected_mode]) && is_array($players_data[$selected_mode])) {
// Determine selected player
$availablePlayers = array_keys($players_data[$selected_mode]);
$selected_player_from_get = $_GET['selected_player'] ?? null;
if ($selected_player_from_get && in_array($selected_player_from_get, $availablePlayers)) {
$selected_player = $selected_player_from_get;
} elseif (!empty($availablePlayers)) {
$selected_player = $availablePlayers[0]; // Default to the first player if none selected or invalid
}
// Fetch the player stats if a player is selected
if ($selected_player && isset($players_data[$selected_mode][$selected_player])) {
$account_id = array_key_first($players_data[$selected_mode][$selected_player]);
if ($account_id && isset($players_data[$selected_mode][$selected_player][$account_id])) {
$stats = $players_data[$selected_mode][$selected_player][$account_id];
} else {
$dataError = "Could not find account ID or stats for the selected player.";
}
} elseif (empty($availablePlayers)) {
$dataError = "No players found for the selected game mode: " . htmlspecialchars($selected_mode);
} else {
$dataError = "Selected player not found or no player selected for this mode.";
}
} else {
$dataError = "Selected game mode (" . htmlspecialchars($selected_mode) . ") not found in data.";
}
}
} else {
$dataError = "Player lifetime data file not found.";
}
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<?php include './includes/head.php'; ?> <?php include './includes/head.php'; // Includes $ogDescription ?>
<body> <body>
<?php <?php
@ -15,53 +80,57 @@ include './includes/header.php';
<main> <main>
<section> <section>
<h2>User Stats</h2> <h2>User Stats</h2>
<?php
include './config/config.php';
$players_data = json_decode(file_get_contents('./data/player_lifetime_data.json'), true); <?php if ($dataError): ?>
<p style="color: red;"><?php echo htmlspecialchars($dataError); ?></p>
<?php endif; ?>
$selected_mode = isset($_GET['game_mode']) ? $_GET['game_mode'] : 'squad'; <!-- Form to select game mode -->
<form method="get" action="">
<?php foreach ($availableModes as $mode): ?>
<input type="submit" name="game_mode" value="<?php echo htmlspecialchars($mode); ?>" class="btn<?php echo ($mode === $selected_mode) ? ' active' : ''; ?>">
<?php endforeach; ?>
<?php if ($selected_player): // Keep selected player if switching modes ?>
<input type="hidden" name="selected_player" value="<?php echo htmlspecialchars($selected_player); ?>">
<?php endif; ?>
</form>
<br>
// Form to select game mode <?php if (isset($players_data[$selected_mode]) && is_array($players_data[$selected_mode]) && !empty($players_data[$selected_mode])): ?>
echo "<form method='get' action=''> <!-- Buttons for each player -->
<input type='submit' name='game_mode' value='solo' class='btn'> <form method="get" action="">
<input type='submit' name='game_mode' value='duo' class='btn'> <?php foreach ($players_data[$selected_mode] as $player_name => $player_details): ?>
<input type='submit' name='game_mode' value='squad' class='btn'> <button type="submit" name="selected_player" value="<?php echo htmlspecialchars($player_name); ?>" class="btn<?php echo ($player_name === $selected_player) ? ' active' : ''; ?>">
<?php echo htmlspecialchars($player_name); ?>
</button>
<?php endforeach; ?>
<input type="hidden" name="game_mode" value="<?php echo htmlspecialchars($selected_mode); ?>">
</form>
<br>
<?php endif; ?>
<input type='submit' name='game_mode' value='solo-fpp' class='btn'> <?php if ($selected_player && $stats): ?>
<input type='submit' name='game_mode' value='duo-fpp' class='btn'> <h2><?php echo htmlspecialchars(ucfirst($selected_mode)); ?> Lifetime Stats for <?php echo htmlspecialchars($selected_player); ?></h2>
<input type='submit' name='game_mode' value='squad-fpp' class='btn'> <table border="1">
</form><br>"; <thead>
<tr><th>Stat Name</th><th>Value</th></tr>
</thead>
<tbody>
<?php foreach ($stats as $stat_name => $stat_value): ?>
<tr>
<td><?php echo htmlspecialchars($stat_name); ?></td>
<td><?php echo htmlspecialchars(is_scalar($stat_value) ? $stat_value : json_encode($stat_value)); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<br>
<?php elseif (!$dataError): // Only show if no major data error occurred ?>
<p>Select a player to view their stats for the <?php echo htmlspecialchars($selected_mode); ?> mode.</p>
<?php endif; ?>
// Buttons for each player <p>Last update: <?php echo $lastUpdated; ?></p>
echo "<form method='get' action=''>";
foreach ($players_data[$selected_mode] as $player_name => $player_details) {
echo "<button type='submit' name='selected_player' value='$player_name' class='btn' >$player_name</button>";
}
echo "<input type='hidden' name='game_mode' value='$selected_mode'>";
echo "</form><br>";
$selected_player = $_GET['selected_player'] ?? array_key_first($players_data[$selected_mode]);
// Fetch the player stats based on game mode and selected player
if (isset($players_data[$selected_mode][$selected_player])) {
$account_id = array_key_first($players_data[$selected_mode][$selected_player]);
$stats = $players_data[$selected_mode][$selected_player][$account_id];
echo "<h2>" . ucfirst($selected_mode) . " Lifetime Stats for $selected_player</h2>";
echo "<table border='1'>";
echo "<tr><th>Stat Name</th><th>Value</th></tr>";
foreach ($stats as $stat_name => $stat_value) {
echo "<tr><td>$stat_name</td><td>$stat_value</td></tr>";
}
echo "</table><br>";
} else {
echo "No player data available.";
}
echo "Last update " ;
echo $players_data['updated'];
?>
</section> </section>
</main> </main>