Add ProjectZomboidServerLog (DebugLog-server.txt)

Concrete Log subclass for the engine debug log. Captures time, level,
and subsystem prefix per entry; stack-trace continuation lines attach
to the triggering ERROR entry via PatternParser's append-on-no-match
behaviour. Detectors: filename match on DebugLog-server.txt plus two
content signatures (the version=X.Y.Z+hash banner and the level/
subsystem/f/t/st header shape). Pattern constants live in
src/Pattern/ProjectZomboid/DebugServerPattern.php with named groups
ready for analyser use in phase B. Synthetic fixture under
test/src/Games/ProjectZomboid/fixtures/ uses zeroed identifiers and
placeholder paths.
This commit is contained in:
2026-04-30 20:34:03 +00:00
parent c032fd34b8
commit d863fae9e6
5 changed files with 170 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<?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\DebugServerPattern;
/**
* Project Zomboid engine debug log (DebugLog-server.txt).
*
* Multi-line format: ERROR entries are followed by tab-indented stack trace
* frames. PatternParser handles continuation by appending non-matching lines
* to the most recent Entry, which is exactly the behaviour we need.
*/
class ProjectZomboidServerLog extends ProjectZomboidLog
{
public static function getDefaultParser(): ParserInterface
{
return static::makePatternParser(
DebugServerPattern::LINE,
[PatternParser::TIME, PatternParser::LEVEL, PatternParser::PREFIX]
);
}
public static function getDefaultAnalyser(): AnalyserInterface
{
return new PatternAnalyser();
}
public static function getDetectors(): array
{
return [
(new FilenameDetector())
->setPattern('/DebugLog-server\.txt$/')
->setWeight(0.95),
(new WeightedSinglePatternDetector())
->setPattern('/version=\d+\.\d+\.\d+ [a-f0-9]{40}/')
->setWeight(0.95),
(new WeightedSinglePatternDetector())
->setPattern('/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] (?:LOG|WARN|ERROR):\s+\w+\s+f:\d+, t:\d+, st:[\d,]+>/m')
->setWeight(0.80),
];
}
public function getTitle(): string
{
return "Project Zomboid Debug Server Log";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace IndifferentKetchup\Codex\Pattern\ProjectZomboid;
/**
* Regex constants for the Project Zomboid DebugLog-server.txt format.
*
* LINE captures, in order:
* 1. time (DD-MM-YY HH:MM:SS.mmm)
* 2. level (LOG | WARN | ERROR | INFO | DEBUG)
* 3. prefix (subsystem name, e.g. General, Mod, WorldGen)
*
* The f:/t:/st: metadata and trailing message body are intentionally not
* captured by the parser; analyzers reach into the Line raw text directly.
*/
class DebugServerPattern
{
public const string LINE = '/^\[(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\]\s+(\w+)\s*:\s+(\S+)\s+f:\d+,\s+t:\d+,\s+st:[\d,]+>\s+.*$/';
public const string VERSION = '/version=(?<version>\S+) (?<hash>[a-f0-9]{40}) (?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2})/';
public const string MOD_LOAD = '/loading (?<mod>[A-Za-z0-9_]+)\.?$/';
public const string MOD_MISSING = '/required mod "(?<mod>[^"]+)" not found/';
public const string EXCEPTION_HEADER = '/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]\s+ERROR:.*Exception thrown/';
}

View File

@@ -0,0 +1,22 @@
[16-04-26 00:00:42.314] LOG : General f:0, t:1776297642254, st:48,648,157,434> SLF4J(W): No SLF4J providers were found..
[16-04-26 00:00:42.315] LOG : General f:0, t:1776297642314, st:48,648,157,492> SLF4J(W): Defaulting to no-operation (NOP) logger implementation.
[16-04-26 00:00:42.407] LOG : General f:0, t:1776297642406, st:48,648,157,584> version=42.16.3 0000000000000000000000000000000000000000 2026-04-08 11:54:01 (ZB) demo=false.
[16-04-26 00:00:42.407] LOG : General f:0, t:1776297642407, st:48,648,157,585> revision=0000000000000000000000000000000000000000 date=2026-04-08 time=11:54:01 (ZB).
[16-04-26 00:01:19.080] ERROR: General f:0, t:1776297679080, st:48,648,194,258> DebugFileWatcher.registerDir> Exception thrown
java.nio.file.NoSuchFileException: /placeholder/config/mods at UnixException.translateToIOException(null:-1).
Stack trace:
java.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)
java.base/sun.nio.fs.UnixException.asIOException(Unknown Source)
java.base/sun.nio.fs.LinuxWatchService$Poller.implRegister(Unknown Source)
java.base/sun.nio.fs.AbstractPoller.processRequests(Unknown Source)
java.base/sun.nio.fs.LinuxWatchService$Poller.run(Unknown Source)
[16-04-26 00:01:19.131] LOG : Mod f:0, t:1776297679131, st:48,648,194,309> loading example_mod_alpha.
[16-04-26 00:01:19.142] LOG : Mod f:0, t:1776297679142, st:48,648,194,320> loading example_mod_beta.
[16-04-26 00:01:19.155] LOG : Mod f:0, t:1776297679155, st:48,648,194,333> loading example_mod_gamma.
[16-04-26 00:01:19.200] WARN : Mod f:0, t:1776297679200, st:48,648,194,378> ZomboidFileSystem.loadModAndRequired> required mod "absent_mod" not found.
[16-04-26 00:01:45.937] ERROR: WorldGen f:0, t:1776297705937, st:48,648,221,115> IsoPropertyType.lookupOrDefaultStr> Exception thrown
zombie.core.properties.IsoPropertyType$IsoPropertyTypeNotFoundException: Property Name not found: ladderW at IsoPropertyType.lookup(IsoPropertyType.java:269). Message: Property Name not found: ladderW
at zombie.core.properties.IsoPropertyType.lookup(IsoPropertyType.java:269)
at zombie.iso.IsoChunkData.PostProcessChunk(IsoChunkData.java:512)
[16-04-26 00:02:00.000] LOG : General f:0, t:1776297720000, st:48,648,235,178> server initialised.
[16-04-26 00:05:00.000] LOG : General f:0, t:1776297900000, st:48,648,415,178> shutdown requested.

View File

@@ -0,0 +1,67 @@
<?php
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Log;
use IndifferentKetchup\Codex\Detective\Detective;
use IndifferentKetchup\Codex\Log\File\PathLogFile;
use IndifferentKetchup\Codex\Log\Level;
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidServerLog;
use PHPUnit\Framework\TestCase;
class ProjectZomboidServerLogTest extends TestCase
{
private function fixturePath(): string
{
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/debug-server-minimal.txt';
}
public function testParsesEntriesWithLevelAndPrefix(): void
{
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
$log->parse();
$entries = $log->getEntries();
$this->assertNotEmpty($entries);
$first = $entries[0];
$this->assertSame('General', $first->getPrefix());
$this->assertSame(Level::INFO, $first->getLevel());
$this->assertNotNull($first->getTime());
}
public function testStackTraceLinesAttachToTriggeringErrorEntry(): void
{
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
$log->parse();
$errorEntry = null;
foreach ($log->getEntries() as $entry) {
if ($entry->getLevel() === Level::ERROR && $entry->getPrefix() === 'General') {
$errorEntry = $entry;
break;
}
}
$this->assertNotNull($errorEntry);
$this->assertGreaterThan(1, count($errorEntry->getLines()));
}
public function testWarnLevelMapsCorrectly(): void
{
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
$log->parse();
$warnEntries = array_filter($log->getEntries(), fn($e) => $e->getLevel() === Level::WARNING);
$this->assertNotEmpty($warnEntries);
}
public function testDetectiveDispatchesByContent(): void
{
$detective = (new Detective())
->setLogFile(new PathLogFile($this->fixturePath()))
->addPossibleLogClass(ProjectZomboidServerLog::class);
$log = $detective->detect();
$this->assertInstanceOf(ProjectZomboidServerLog::class, $log);
}
}