From 73e9ca61810df16f3779ba74c7b45f6d362b8e98 Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 30 Apr 2026 22:39:13 +0000 Subject: [PATCH] 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. --- src/Analyser/ProjectZomboid/.gitkeep | 0 .../ConnectionFailureAnalyser.php | 64 ++++++++++++++++++ .../ConnectionFailureProblem.php | 67 +++++++++++++++++++ .../ProjectZomboid/ProjectZomboidUserLog.php | 4 +- .../Analyser/UserLogAnalysisTest.php | 43 ++++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) delete mode 100644 src/Analyser/ProjectZomboid/.gitkeep create mode 100644 src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php create mode 100644 src/Analysis/ProjectZomboid/ConnectionFailureProblem.php create mode 100644 test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php diff --git a/src/Analyser/ProjectZomboid/.gitkeep b/src/Analyser/ProjectZomboid/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php b/src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php new file mode 100644 index 0000000..198250f --- /dev/null +++ b/src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php @@ -0,0 +1,64 @@ +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; + } +} diff --git a/src/Analysis/ProjectZomboid/ConnectionFailureProblem.php b/src/Analysis/ProjectZomboid/ConnectionFailureProblem.php new file mode 100644 index 0000000..8e9752d --- /dev/null +++ b/src/Analysis/ProjectZomboid/ConnectionFailureProblem.php @@ -0,0 +1,67 @@ +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; + } +} diff --git a/src/Log/ProjectZomboid/ProjectZomboidUserLog.php b/src/Log/ProjectZomboid/ProjectZomboidUserLog.php index fe9e48d..aca210f 100644 --- a/src/Log/ProjectZomboid/ProjectZomboidUserLog.php +++ b/src/Log/ProjectZomboid/ProjectZomboidUserLog.php @@ -3,7 +3,7 @@ namespace IndifferentKetchup\Codex\Log\ProjectZomboid; 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\WeightedSinglePatternDetector; use IndifferentKetchup\Codex\Parser\ParserInterface; @@ -22,7 +22,7 @@ class ProjectZomboidUserLog extends ProjectZomboidEventLog public static function getDefaultAnalyser(): AnalyserInterface { - return new PatternAnalyser(); + return new ConnectionFailureAnalyser(); } public static function getDetectors(): array diff --git a/test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php b/test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php new file mode 100644 index 0000000..56c03e3 --- /dev/null +++ b/test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php @@ -0,0 +1,43 @@ +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()); + } + } +}