From af05c97dfc29f92b50c72486f77f08b49300ec3d Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Thu, 30 Apr 2026 20:40:03 +0000 Subject: [PATCH] Add ProjectZomboidPvpLog (pvp.txt) Two row variants share the file: Safe House toggles ([LOG] Safety:) and Combat events ([INFO] Combat: ... weapon=... damage=...). Parser captures time, level, and the subsystem token (Safety|Combat) as the entry prefix. COMBAT and SAFETY regexes extract structured fields, including support for negative Z coordinates from basement levels. Synthetic fixture covers both variants and represents zombie/vehicle/ real-PvP weapon types so analysers can later filter on damage>0 and weapon!=zombie. --- .../ProjectZomboid/ProjectZomboidPvpLog.php | 47 +++++++++++++ src/Pattern/ProjectZomboid/PvpPattern.php | 27 ++++++++ .../ProjectZomboid/fixtures/pvp-minimal.txt | 10 +++ .../Log/ProjectZomboidPvpLogTest.php | 69 +++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/Log/ProjectZomboid/ProjectZomboidPvpLog.php create mode 100644 src/Pattern/ProjectZomboid/PvpPattern.php create mode 100644 test/src/Games/ProjectZomboid/fixtures/pvp-minimal.txt create mode 100644 test/tests/Games/ProjectZomboid/Log/ProjectZomboidPvpLogTest.php diff --git a/src/Log/ProjectZomboid/ProjectZomboidPvpLog.php b/src/Log/ProjectZomboid/ProjectZomboidPvpLog.php new file mode 100644 index 0000000..37760d8 --- /dev/null +++ b/src/Log/ProjectZomboid/ProjectZomboidPvpLog.php @@ -0,0 +1,47 @@ +setPattern('/_pvp\.txt$/') + ->setWeight(0.95), + (new WeightedSinglePatternDetector()) + ->setPattern('/^\[[^\]]+\]\[\w+\] Combat: "[^"]+" \(/m') + ->setWeight(0.95), + (new WeightedSinglePatternDetector()) + ->setPattern('/^\[[^\]]+\]\[\w+\] Safety: "/m') + ->setWeight(0.85), + ]; + } + + public function getTitle(): string + { + return "Project Zomboid PvP Log"; + } +} diff --git a/src/Pattern/ProjectZomboid/PvpPattern.php b/src/Pattern/ProjectZomboid/PvpPattern.php new file mode 100644 index 0000000..a55bc77 --- /dev/null +++ b/src/Pattern/ProjectZomboid/PvpPattern.php @@ -0,0 +1,27 @@ +[^"]+)" \((?\d+),(?\d+),(?-?\d+)\) hit "(?[^"]+)" \((?\d+),(?\d+),(?-?\d+)\) weapon="(?[^"]+)" damage=(?-?\d+\.\d+)\.$/'; + + public const string SAFETY = '/^Safety: "(?[^"]+)" \((?\d+),(?\d+),(?-?\d+)\) (?\w+) (?true|false)\.$/'; +} diff --git a/test/src/Games/ProjectZomboid/fixtures/pvp-minimal.txt b/test/src/Games/ProjectZomboid/fixtures/pvp-minimal.txt new file mode 100644 index 0000000..865c859 --- /dev/null +++ b/test/src/Games/ProjectZomboid/fixtures/pvp-minimal.txt @@ -0,0 +1,10 @@ +[16-04-26 16:17:49.731][LOG] Safety: "Player1" (1000,2000,0) restore true. +[16-04-26 16:18:12.078][LOG] Safety: "Player2" (1010,2010,0) restore true. +[16-04-26 16:20:34.144][LOG] Safety: "Player1" (1000,2000,0) store true. +[16-04-26 16:25:41.402][LOG] Safety: "AdminUser" (1020,2020,0) restore true. +[16-04-26 16:36:07.978][LOG] Safety: "Player2" (1011,2011,0) store true. +[16-04-26 17:14:28.814][INFO] Combat: "Player1" (1005,2005,0) hit "Player2" (1005,2005,0) weapon="Bare Hands" damage=0.026675. +[16-04-26 17:14:35.128][INFO] Combat: "Player1" (1005,2005,0) hit "Player2" (1006,2005,0) weapon="Tire Iron (Worn)" damage=0.112317. +[16-04-26 17:42:49.022][INFO] Combat: "Player2" (1005,2005,0) hit "Player2" (1005,2005,0) weapon="zombie" damage=-1.000000. +[16-04-26 17:45:14.028][INFO] Combat: "Player1" (1100,2200,0) hit "Player2" (1100,2201,0) weapon="vehicle" damage=0.000000. +[16-04-26 18:00:01.515][INFO] Combat: "AdminUser" (1020,2020,-1) hit "Player1" (1020,2020,-1) weapon="Hunting Knife" damage=0.350000. diff --git a/test/tests/Games/ProjectZomboid/Log/ProjectZomboidPvpLogTest.php b/test/tests/Games/ProjectZomboid/Log/ProjectZomboidPvpLogTest.php new file mode 100644 index 0000000..624ca7b --- /dev/null +++ b/test/tests/Games/ProjectZomboid/Log/ProjectZomboidPvpLogTest.php @@ -0,0 +1,69 @@ +setLogFile(new PathLogFile($this->fixturePath())); + $log->parse(); + + $this->assertCount(10, $log->getEntries()); + } + + public function testSubsystemIsCapturedAsPrefix(): void + { + $log = (new ProjectZomboidPvpLog())->setLogFile(new PathLogFile($this->fixturePath())); + $log->parse(); + + $entries = $log->getEntries(); + $this->assertSame('Safety', $entries[0]->getPrefix()); + $this->assertSame('Combat', $entries[5]->getPrefix()); + } + + public function testCombatRegexExtractsRealPvpDamage(): void + { + $line = 'Combat: "Player1" (1005,2005,0) hit "Player2" (1006,2005,0) weapon="Tire Iron (Worn)" damage=0.112317.'; + $this->assertSame(1, preg_match(PvpPattern::COMBAT, $line, $m)); + $this->assertSame('Player1', $m['attacker']); + $this->assertSame('Player2', $m['victim']); + $this->assertSame('Tire Iron (Worn)', $m['weapon']); + $this->assertSame('0.112317', $m['damage']); + } + + public function testCombatRegexHandlesNegativeZ(): void + { + $line = 'Combat: "AdminUser" (1020,2020,-1) hit "Player1" (1020,2020,-1) weapon="Hunting Knife" damage=0.350000.'; + $this->assertSame(1, preg_match(PvpPattern::COMBAT, $line, $m)); + $this->assertSame('-1', $m['az']); + } + + public function testSafetyRegexExtracts(): void + { + $line = 'Safety: "Player1" (1000,2000,0) restore true.'; + $this->assertSame(1, preg_match(PvpPattern::SAFETY, $line, $m)); + $this->assertSame('Player1', $m['player']); + $this->assertSame('restore', $m['verb']); + } + + public function testDetectiveDispatchesByContent(): void + { + $detective = (new Detective()) + ->setLogFile(new PathLogFile($this->fixturePath())) + ->addPossibleLogClass(ProjectZomboidPvpLog::class); + + $this->assertInstanceOf(ProjectZomboidPvpLog::class, $detective->detect()); + } +}