diff --git a/src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php b/src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php new file mode 100644 index 0000000..f525703 --- /dev/null +++ b/src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php @@ -0,0 +1,90 @@ +setLog($this->log); + + $groups = []; + foreach ($this->log as $entry) { + if (preg_match(ItemPattern::FIELDS, (string) $entry, $m) !== 1) { + continue; + } + if (!str_starts_with($m['delta'], '+')) { + continue; + } + $key = $m['steamid'] . '|' . $m['item']; + $groups[$key][] = [ + 'time' => $entry->getTime() ?? 0, + 'steamid' => $m['steamid'], + 'item' => $m['item'], + 'player' => $m['player'], + ]; + } + + foreach ($groups as $events) { + usort($events, static fn($a, $b) => $a['time'] <=> $b['time']); + + $left = 0; + $eventCount = count($events); + for ($right = 0; $right < $eventCount; $right++) { + while ($events[$right]['time'] - $events[$left]['time'] > self::THRESHOLD_WINDOW_SECONDS) { + $left++; + } + if (($right - $left + 1) >= self::THRESHOLD_COUNT) { + $sample = $events[0]; + $analysis->addInsight((new ItemDuplicationProblem()) + ->setSteamId($sample['steamid']) + ->setPlayer($sample['player']) + ->setItem($sample['item']) + ->setEventCount($eventCount)); + break; + } + } + } + + return $analysis; + } +} diff --git a/src/Analysis/ProjectZomboid/ItemDuplicationProblem.php b/src/Analysis/ProjectZomboid/ItemDuplicationProblem.php new file mode 100644 index 0000000..7f5c414 --- /dev/null +++ b/src/Analysis/ProjectZomboid/ItemDuplicationProblem.php @@ -0,0 +1,82 @@ +steamId = $steamId; + return $this; + } + + public function setPlayer(string $player): static + { + $this->player = $player; + return $this; + } + + public function setItem(string $item): static + { + $this->item = $item; + return $this; + } + + public function setEventCount(int $count): static + { + $this->eventCount = $count; + return $this; + } + + public function getSteamId(): string + { + return $this->steamId; + } + + public function getPlayer(): string + { + return $this->player; + } + + public function getItem(): string + { + return $this->item; + } + + public function getEventCount(): int + { + return $this->eventCount; + } + + public function getMessage(): string + { + return sprintf( + 'Player %s (%s) gained %s %d times at a rate above the duplication threshold.', + $this->player, + $this->steamId, + $this->item, + $this->eventCount + ); + } + + public function isEqual(InsightInterface $insight): bool + { + return $insight instanceof self + && $insight->getSteamId() === $this->steamId + && $insight->getItem() === $this->item; + } +} diff --git a/src/Log/ProjectZomboid/ProjectZomboidItemLog.php b/src/Log/ProjectZomboid/ProjectZomboidItemLog.php index d7ebc9c..7a831b0 100644 --- a/src/Log/ProjectZomboid/ProjectZomboidItemLog.php +++ b/src/Log/ProjectZomboid/ProjectZomboidItemLog.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\ItemDuplicationAnalyser; use IndifferentKetchup\Codex\Detective\FilenameDetector; use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector; use IndifferentKetchup\Codex\Parser\ParserInterface; @@ -22,7 +22,7 @@ class ProjectZomboidItemLog extends ProjectZomboidEventLog public static function getDefaultAnalyser(): AnalyserInterface { - return new PatternAnalyser(); + return new ItemDuplicationAnalyser(); } public static function getDetectors(): array diff --git a/test/src/Games/ProjectZomboid/fixtures/item-minimal.txt b/test/src/Games/ProjectZomboid/fixtures/item-minimal.txt index 96bbad7..e88ff6c 100644 --- a/test/src/Games/ProjectZomboid/fixtures/item-minimal.txt +++ b/test/src/Games/ProjectZomboid/fixtures/item-minimal.txt @@ -8,3 +8,13 @@ [16-04-26 19:35:42.812] 76561198000000001 "Player1" container -1 1002,2002,0 [Base.WaterBottleFull]. [16-04-26 19:40:00.514] 76561198000000002 "Player2" floor +1 1011,2011,0 [Base.Bandage]. [16-04-26 19:42:25.223] 76561198000000002 "Player2" inventory +5 1011,2011,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.001] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.002] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.003] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.004] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.005] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 19:50:00.006] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm]. +[16-04-26 20:00:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank]. +[16-04-26 20:01:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank]. +[16-04-26 20:02:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank]. +[16-04-26 20:03:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank]. diff --git a/test/tests/Games/ProjectZomboid/Analyser/ItemLogAnalysisTest.php b/test/tests/Games/ProjectZomboid/Analyser/ItemLogAnalysisTest.php new file mode 100644 index 0000000..95bd079 --- /dev/null +++ b/test/tests/Games/ProjectZomboid/Analyser/ItemLogAnalysisTest.php @@ -0,0 +1,51 @@ +setLogFile(new PathLogFile($this->fixturePath())); + $log->parse(); + $analysis = $log->analyse(); + + $problems = $analysis->getFilteredInsights(ItemDuplicationProblem::class); + $this->assertCount(1, $problems); + + $problem = $problems[0]; + $this->assertSame('76561198000000003', $problem->getSteamId()); + $this->assertSame('AdminUser', $problem->getPlayer()); + $this->assertSame('Base.Bullets9mm', $problem->getItem()); + $this->assertSame(6, $problem->getEventCount()); + } + + public function testDoesNotFlagSubThresholdGroup(): void + { + $log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath())); + $log->parse(); + $analysis = $log->analyse(); + + $problems = $analysis->getFilteredInsights(ItemDuplicationProblem::class); + foreach ($problems as $problem) { + $this->assertNotSame('Base.Plank', $problem->getItem()); + } + } + + public function testThresholdConstantsAreDocumentedAndPositive(): void + { + $this->assertGreaterThan(0, ItemDuplicationAnalyser::THRESHOLD_COUNT); + $this->assertGreaterThan(0, ItemDuplicationAnalyser::THRESHOLD_WINDOW_SECONDS); + } +} diff --git a/test/tests/Games/ProjectZomboid/Log/ProjectZomboidItemLogTest.php b/test/tests/Games/ProjectZomboid/Log/ProjectZomboidItemLogTest.php index 79062b7..3a3cba2 100644 --- a/test/tests/Games/ProjectZomboid/Log/ProjectZomboidItemLogTest.php +++ b/test/tests/Games/ProjectZomboid/Log/ProjectZomboidItemLogTest.php @@ -20,7 +20,7 @@ class ProjectZomboidItemLogTest extends TestCase $log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath())); $log->parse(); - $this->assertCount(10, $log->getEntries()); + $this->assertCount(20, $log->getEntries()); } public function testFieldsRegexExtractsItemAndDelta(): void