AI
This commit is contained in:
parent
1be9732279
commit
01aa843d0b
18 changed files with 4122 additions and 2393 deletions
160
demo.php
160
demo.php
|
|
@ -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>
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -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
256
index.php
|
|
@ -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>
|
||||||
303
last_stats.php
303
last_stats.php
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
674
lib/sorttable.js
674
lib/sorttable.js
|
|
@ -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 ? ' <font face="webdings">5</font>' : ' ▴';
|
|
||||||
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 ? ' <font face="webdings">6</font>' : ' ▾';
|
|
||||||
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 ? ' <font face="webdings">6</font>' : ' ▾';
|
|
||||||
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' ? ' ▾' : ' ▴'; // 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
335
matchinfo.php
335
matchinfo.php
|
|
@ -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>
|
||||||
147
topstats.php
147
topstats.php
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
152
topstatsavg.php
152
topstatsavg.php
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
155
user_stats.php
155
user_stats.php
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue