Add ConnectionFailureAnalyser
First custom Analyser subclass in this game tree. PatternAnalyser operates per-entry without cross-entry state, so pairing 'attempting to join' with 'allowed to join' per Steam ID requires a bespoke pass over the log. The analyser counts attempts and allowed events per Steam ID and emits a ConnectionFailureProblem for each player whose attempt count exceeds their allowed count. Unmatched 'attempting to join used queue' rows are surfaced as failures in v1 because a long queue wait is indistinguishable from a real failure without timing context.
This commit is contained in:
64
src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php
Normal file
64
src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analyser\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\Analyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Analysis;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ConnectionFailureProblem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\UserPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairs "attempting to join" with subsequent "allowed to join" events per
|
||||||
|
* Steam ID and flags any unmatched attempts. PatternAnalyser cannot express
|
||||||
|
* this because it operates per-entry without cross-entry state, so this
|
||||||
|
* walks the entire log once and aggregates before emitting Problems.
|
||||||
|
*
|
||||||
|
* "attempting to join used queue" is treated as an attempt; a player still
|
||||||
|
* waiting in queue at end-of-log will therefore be flagged. This is
|
||||||
|
* intentional v1 behaviour — a long-lived queue wait looks indistinguishable
|
||||||
|
* from a real failure without timing context, and surfacing both lets a
|
||||||
|
* human triage.
|
||||||
|
*/
|
||||||
|
class ConnectionFailureAnalyser extends Analyser
|
||||||
|
{
|
||||||
|
public function analyse(): AnalysisInterface
|
||||||
|
{
|
||||||
|
$analysis = new Analysis();
|
||||||
|
$analysis->setLog($this->log);
|
||||||
|
|
||||||
|
$attempts = [];
|
||||||
|
$allowed = [];
|
||||||
|
$playerName = [];
|
||||||
|
|
||||||
|
foreach ($this->log as $entry) {
|
||||||
|
$text = (string) $entry;
|
||||||
|
if (preg_match(UserPattern::PLAYER_EVENT, $text, $m) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$steamId = $m['steamid'];
|
||||||
|
$playerName[$steamId] = $m['player'];
|
||||||
|
|
||||||
|
if (str_starts_with($m['event'], 'attempting to join')) {
|
||||||
|
$attempts[$steamId] = ($attempts[$steamId] ?? 0) + 1;
|
||||||
|
} elseif (str_starts_with($m['event'], 'allowed to join')) {
|
||||||
|
$allowed[$steamId] = ($allowed[$steamId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($attempts as $steamId => $attemptCount) {
|
||||||
|
$allowedCount = $allowed[$steamId] ?? 0;
|
||||||
|
$unmatched = $attemptCount - $allowedCount;
|
||||||
|
if ($unmatched <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysis->addInsight((new ConnectionFailureProblem())
|
||||||
|
->setSteamId($steamId)
|
||||||
|
->setPlayer($playerName[$steamId] ?? '')
|
||||||
|
->setUnmatchedAttempts($unmatched));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $analysis;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Analysis/ProjectZomboid/ConnectionFailureProblem.php
Normal file
67
src/Analysis/ProjectZomboid/ConnectionFailureProblem.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problem emitted by ConnectionFailureAnalyser when a player's
|
||||||
|
* "attempting to join" event count exceeds their "allowed to join" count
|
||||||
|
* within the same log file. Coalesced by Steam ID so each player produces
|
||||||
|
* at most one problem regardless of how many unmatched attempts they have.
|
||||||
|
*/
|
||||||
|
class ConnectionFailureProblem extends Problem
|
||||||
|
{
|
||||||
|
private string $steamId = '';
|
||||||
|
private string $player = '';
|
||||||
|
private int $unmatchedAttempts = 0;
|
||||||
|
|
||||||
|
public function setSteamId(string $steamId): static
|
||||||
|
{
|
||||||
|
$this->steamId = $steamId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayer(string $player): static
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUnmatchedAttempts(int $count): static
|
||||||
|
{
|
||||||
|
$this->unmatchedAttempts = $count;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSteamId(): string
|
||||||
|
{
|
||||||
|
return $this->steamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayer(): string
|
||||||
|
{
|
||||||
|
return $this->player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnmatchedAttempts(): int
|
||||||
|
{
|
||||||
|
return $this->unmatchedAttempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Player %s (%s) had %d "attempting to join" event(s) without a matching "allowed to join".',
|
||||||
|
$this->player,
|
||||||
|
$this->steamId,
|
||||||
|
$this->unmatchedAttempts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self && $insight->getSteamId() === $this->steamId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ConnectionFailureAnalyser;
|
||||||
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
||||||
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
||||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
@@ -22,7 +22,7 @@ class ProjectZomboidUserLog extends ProjectZomboidEventLog
|
|||||||
|
|
||||||
public static function getDefaultAnalyser(): AnalyserInterface
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
{
|
{
|
||||||
return new PatternAnalyser();
|
return new ConnectionFailureAnalyser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDetectors(): array
|
public static function getDetectors(): array
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ConnectionFailureProblem;
|
||||||
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidUserLog;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class UserLogAnalysisTest extends TestCase
|
||||||
|
{
|
||||||
|
private function fixturePath(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/user-minimal.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlagsPlayerWithUnmatchedAttempts(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidUserLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ConnectionFailureProblem::class);
|
||||||
|
$this->assertCount(1, $problems);
|
||||||
|
|
||||||
|
$problem = $problems[0];
|
||||||
|
$this->assertSame('76561198000000001', $problem->getSteamId());
|
||||||
|
$this->assertSame('Player1', $problem->getPlayer());
|
||||||
|
$this->assertSame(1, $problem->getUnmatchedAttempts());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotFlagPlayerWithMatchedAttempts(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidUserLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ConnectionFailureProblem::class);
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertNotSame('76561198000000002', $problem->getSteamId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user