Add ProjectZomboidPerkLog (PerkLog.txt)

Per-line skill snapshot log; each Login event is paired with a perks
row containing comma-separated Skill=N tokens. PERK_PAIR regex extracts
each pair via preg_match_all for analyser use. Detectors: filename
match plus content signature on the unique '[Cooking=N, Fitness=N,
Strength=N,' prefix of the perks-row bracket.
This commit is contained in:
2026-04-30 20:38:39 +00:00
parent 6387fb1c52
commit 00c17261a3
4 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
use IndifferentKetchup\Codex\Detective\FilenameDetector;
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
use IndifferentKetchup\Codex\Parser\ParserInterface;
use IndifferentKetchup\Codex\Parser\PatternParser;
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\PerkPattern;
class ProjectZomboidPerkLog extends ProjectZomboidEventLog
{
public static function getDefaultParser(): ParserInterface
{
return static::makePatternParser(
PerkPattern::LINE,
[PatternParser::TIME]
);
}
public static function getDefaultAnalyser(): AnalyserInterface
{
return new PatternAnalyser();
}
public static function getDetectors(): array
{
return [
(new FilenameDetector())
->setPattern('/_PerkLog\.txt$/')
->setWeight(0.95),
(new WeightedSinglePatternDetector())
->setPattern('/\[Cooking=\d+, Fitness=\d+, Strength=\d+,/')
->setWeight(0.95),
];
}
public function getTitle(): string
{
return "Project Zomboid Perk Log";
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace IndifferentKetchup\Codex\Pattern\ProjectZomboid;
/**
* Regex constants for the Project Zomboid PerkLog.txt format.
*
* [time] [steamid][player][x,y,z][event-or-perks][Hours Survived: N].
*
* The fourth bracketed field is either a single token (Login, Logout,
* LevelUp, etc.) or a comma-separated list of Skill=N pairs. Both fit
* the same character class.
*/
class PerkPattern
{
public const string LINE = '/^\[(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\] \[\d{17}\]\[[^\]]+\]\[\d+,\d+,\d+\]\[[^\]]+\]\[Hours Survived: \d+\]\.$/';
public const string FIELDS = '/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] \[(?<steamid>\d{17})\]\[(?<player>[^\]]+)\]\[(?<x>\d+),(?<y>\d+),(?<z>\d+)\]\[(?<event>[^\]]+)\]\[Hours Survived: (?<hours>\d+)\]\.$/';
public const string PERK_PAIR = '/(?<skill>[A-Za-z_]+)=(?<level>\d+)/';
}

View File

@@ -0,0 +1,6 @@
[16-04-26 18:29:08.171] [76561198000000001][Player1][1000,2000,1][Login][Hours Survived: 100].
[16-04-26 18:29:08.171] [76561198000000001][Player1][1000,2000,1][Cooking=5, Fitness=6, Strength=7, Blunt=7, Axe=0, Lightfoot=0, Nimble=3, Sprinting=5, Sneak=3, Woodwork=10, Aiming=1, Reloading=1, Farming=1, Fishing=1, Trapping=0, PlantScavenging=1, Doctor=1, Electricity=5, Blacksmith=6, MetalWelding=3, Mechanics=6, Spear=0, Maintenance=7, SmallBlade=0, LongBlade=0, SmallBlunt=3, Tailoring=10, Tracking=0, Husbandry=10, FlintKnapping=0, Masonry=6, Pottery=4, Carving=10, Butchering=6, Glassmaking=2, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 100].
[16-04-26 18:29:15.822] [76561198000000002][Player2][1010,2010,0][Login][Hours Survived: 50].
[16-04-26 18:29:15.823] [76561198000000002][Player2][1010,2010,0][Cooking=2, Fitness=10, Strength=10, Blunt=10, Axe=0, Lightfoot=4, Nimble=4, Sprinting=7, Sneak=2, Woodwork=6, Aiming=5, Reloading=4, Farming=0, Fishing=0, Trapping=0, PlantScavenging=0, Doctor=2, Electricity=5, Blacksmith=8, MetalWelding=10, Mechanics=10, Spear=0, Maintenance=7, SmallBlade=0, LongBlade=0, SmallBlunt=0, Tailoring=0, Tracking=0, Husbandry=0, FlintKnapping=0, Masonry=0, Pottery=0, Carving=0, Butchering=0, Glassmaking=0, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 50].
[16-04-26 18:30:02.500] [76561198000000003][AdminUser][1020,2020,0][Logout][Hours Survived: 75].
[16-04-26 19:15:00.000] [76561198000000001][Player1][1003,2003,1][LevelUp][Hours Survived: 101].

View File

@@ -0,0 +1,59 @@
<?php
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Log;
use IndifferentKetchup\Codex\Detective\Detective;
use IndifferentKetchup\Codex\Log\File\PathLogFile;
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidPerkLog;
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\PerkPattern;
use PHPUnit\Framework\TestCase;
class ProjectZomboidPerkLogTest extends TestCase
{
private function fixturePath(): string
{
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/perk-minimal.txt';
}
public function testParsesEachLineAsAnEntry(): void
{
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
$log->parse();
$this->assertCount(6, $log->getEntries());
}
public function testFieldsRegexHandlesEventRow(): void
{
$line = '[16-04-26 18:29:08.171] [76561198000000001][Player1][1000,2000,1][Login][Hours Survived: 100].';
$this->assertSame(1, preg_match(PerkPattern::FIELDS, $line, $m));
$this->assertSame('Player1', $m['player']);
$this->assertSame('Login', $m['event']);
$this->assertSame('100', $m['hours']);
}
public function testFieldsRegexHandlesPerksRow(): void
{
$line = '[16-04-26 18:30:02.500] [76561198000000003][AdminUser][1020,2020,0][Logout][Hours Survived: 75].';
$this->assertSame(1, preg_match(PerkPattern::FIELDS, $line, $m));
$this->assertSame('Logout', $m['event']);
}
public function testPerkPairRegexExtractsSkillsFromBracketedList(): void
{
$bracket = 'Cooking=5, Fitness=6, Strength=7';
$count = preg_match_all(PerkPattern::PERK_PAIR, $bracket, $matches, PREG_SET_ORDER);
$this->assertSame(3, $count);
$this->assertSame('Cooking', $matches[0]['skill']);
$this->assertSame('5', $matches[0]['level']);
}
public function testDetectiveDispatchesByContent(): void
{
$detective = (new Detective())
->setLogFile(new PathLogFile($this->fixturePath()))
->addPossibleLogClass(ProjectZomboidPerkLog::class);
$this->assertInstanceOf(ProjectZomboidPerkLog::class, $detective->detect());
}
}