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:
54
src/Log/ProjectZomboid/ProjectZomboidServerLog.php
Normal file
54
src/Log/ProjectZomboid/ProjectZomboidServerLog.php
Normal 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";
|
||||
}
|
||||
}
|
||||
27
src/Pattern/ProjectZomboid/DebugServerPattern.php
Normal file
27
src/Pattern/ProjectZomboid/DebugServerPattern.php
Normal 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/';
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user