Add ItemDuplicationAnalyser

Sliding-window heuristic over (Steam ID, item code) groups: any window of
THRESHOLD_WINDOW_SECONDS containing THRESHOLD_COUNT or more positive-delta
events for the same player/item pair triggers a Problem. Negative deltas
(drops, transfers out) are filtered. Five events in ten seconds (defaults)
encodes the rule of thumb that legitimate gameplay rarely produces five
identical items in that span.

Constants live as class constants on the analyser so operators can
override via subclass without touching analysis logic; the docblocks
record the justification.

Synthetic fixture extended with a 6-event burst (AdminUser +
Base.Bullets9mm in <1s) and a 4-event sub-threshold group (Player1 +
Base.Plank scattered over 4 minutes) to exercise both paths.
This commit is contained in:
2026-04-30 22:41:36 +00:00
parent 73e9ca6181
commit ba3fae8736
6 changed files with 236 additions and 3 deletions

View File

@@ -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].

View File

@@ -0,0 +1,51 @@
<?php
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ItemDuplicationAnalyser;
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ItemDuplicationProblem;
use IndifferentKetchup\Codex\Log\File\PathLogFile;
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidItemLog;
use PHPUnit\Framework\TestCase;
class ItemLogAnalysisTest extends TestCase
{
private function fixturePath(): string
{
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/item-minimal.txt';
}
public function testFlagsBurstOfSameItemAboveThreshold(): void
{
$log = (new ProjectZomboidItemLog())->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);
}
}

View File

@@ -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