Compare commits
56 Commits
7c7fe5ca80
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 52ff8cb3fe | |||
| 1485507c8f | |||
| ed920485dc | |||
| b99d8f3061 | |||
| 38fa1471ba | |||
| 1cdc78c54c | |||
| 60f12bc868 | |||
| 0c90e40a28 | |||
| ba3fae8736 | |||
| 73e9ca6181 | |||
| c444e8543b | |||
| c57d646229 | |||
| 51eb2de282 | |||
| d15fc81f9f | |||
| 64641fa8e8 | |||
| b7b89ef24e | |||
| caed04db10 | |||
| a2faa551a1 | |||
| 0d85a05df3 | |||
| 90c85a052f | |||
| 55f769ca1e | |||
| df62da1d6e | |||
| 3db825cfdc | |||
| 423c6d3963 | |||
| 1d09358e7b | |||
| 4be6ebac10 | |||
| 11efa66494 | |||
| e45fd85665 | |||
| 499f4c7211 | |||
| 1a443df662 | |||
| 3640ca8291 | |||
| cca5208cc0 | |||
| 27424f6a14 | |||
| d7c36ffc07 | |||
| 7b3342b3d2 | |||
| af05c97dfc | |||
| 00c17261a3 | |||
| 6387fb1c52 | |||
| 49cf4927f6 | |||
| cc9c512667 | |||
| e74c105625 | |||
| 28e8fc8dc6 | |||
| d863fae9e6 | |||
| c032fd34b8 | |||
| ada3c7875d | |||
| 8ae7da5259 | |||
| e709389e08 | |||
| 49249176fc | |||
| 484d3b88a3 | |||
| e1df5cbfd8 | |||
| c9956be7a2 | |||
| 30750ae9d1 | |||
| 9e124f716b | |||
| aae016d17e | |||
| 66a2fcc5f3 | |||
| 6870ed6ea7 |
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set composer cache directory
|
- name: Set composer cache directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Restore composer from cache
|
- name: Restore composer from cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
.idea
|
.idea
|
||||||
vendor
|
vendor
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
|
Logs.zip
|
||||||
|
.scratch/
|
||||||
|
.claude/
|
||||||
|
.claude.local.md
|
||||||
|
|||||||
39
CHANGELOG.md
Normal file
39
CHANGELOG.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to `indifferentketchup/codex` are documented here.
|
||||||
|
|
||||||
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.1.0] — 2026-05-01
|
||||||
|
|
||||||
|
First public release. Codex is a generic PHP log parsing and analysis framework with full Project Zomboid server-log support across eight analysers. The Composer package name is `indifferentketchup/codex` (the repository directory and Gitea slug are `ik-codex`; the package name is not).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Framework foundation** — generic `Log` / `Entry` / `Line` / `Parser` / `Analyser` / `Detective` / `Insight` pipeline forked from upstream `aternos/codex` and renamed end-to-end to `IndifferentKetchup\Codex\*` in `66a2fcc`. Zero `Aternos\Codex\*` namespace references remain in `src/` or `test/`.
|
||||||
|
- **`FilenameDetector`** at `IndifferentKetchup\Codex\Detective\FilenameDetector` — path-based detector that uses the new `LogFileInterface::getPath()` accessor to dispatch on a filename hint. Falls back to `false` for path-less log files (`StringLogFile`, `StreamLogFile`).
|
||||||
|
- **Project Zomboid log subclasses (11)** under `IndifferentKetchup\Codex\Log\ProjectZomboid\*` covering every PZ server-log file type: a multi-line `ProjectZomboidServerLog` for `DebugLog-server.txt`, an abstract `ProjectZomboidEventLog` base for the ten single-line logs, and concrete subclasses for `admin.txt`, `BurdJournals.txt`, `chat.txt`, `ClientActionLog.txt`, `cmd.txt`, `item.txt`, `map.txt`, `PerkLog.txt`, `pvp.txt`, `user.txt`.
|
||||||
|
- **Pattern classes (11)** under `IndifferentKetchup\Codex\Pattern\ProjectZomboid\*` holding regex string constants. Each `<Type>Pattern` carries a `LINE` regex used by `PatternParser`, plus named-group extractor regexes (`FIELDS`, `COMBAT`, `MOD_LOAD`, etc.) used by analysers.
|
||||||
|
- **`ProjectZomboidDetective`** at `IndifferentKetchup\Codex\Detective\ProjectZomboid\ProjectZomboidDetective` — pre-registers all 11 log subclasses in its constructor with paired filename-hint plus content-signature detectors.
|
||||||
|
- **Phase B.1 ServerLog analysers (3)**: `EngineVersionAnalyser` (extracts engine version, build hash, and build date from the server banner), `ModLoadAnalyser` (mod load order plus missing-mod problems with attached `ModMissingSolution`), `ServerExceptionAnalyser` (Java exception type and stack-trace body, coalesced by exception type).
|
||||||
|
- **Phase B.2 PvP and Admin analysers (2)**: `PvpDamageAnalyser` (filters zombie hits and zero-damage rows at the regex itself), `AdminAuditAnalyser` (verb-pattern dispatch across six admin actions: added item, added xp, granted access, changed option, reloaded options, teleported).
|
||||||
|
- **Phase B.3 deferred analysers (3)** — first custom `Analyser` subclasses in the tree, addressing logic that vanilla `PatternAnalyser` cannot express: `ConnectionFailureAnalyser` (event pairing across the file), `ItemDuplicationAnalyser` (sliding-window heuristic with `THRESHOLD_COUNT=5`, `THRESHOLD_WINDOW_SECONDS=10`), `SkillProgressionAnomalyAnalyser` (consecutive-snapshot delta with `THRESHOLD_DELTA=3`). All three threshold constants ship with rationale docblocks and are tunable via subclass override.
|
||||||
|
- **Synthetic test fixtures** under `test/src/Games/ProjectZomboid/fixtures/`, hand-crafted from observed PZ log shapes with placeholder identifiers per the project's privacy rules: Steam IDs `76561198000000001`–`76561198000000004`, names `Player1` / `Player2` / `AdminUser` / `PlayerSuspect`, generic coords. No real-log content reaches the index.
|
||||||
|
- **End-to-end tests** validating each Log subclass's parser, each analyser's insight emission, and the Detective's dispatch behaviour against the synthetic fixtures. Final count: **195 tests, 412 assertions**.
|
||||||
|
- **Project documentation**: `CLAUDE.md` with framework architecture, pitfalls, and workflow conventions; `README.md` with worked Project Zomboid example and per-game support table; design specs and as-built plans for Phase B.1 / B.2 / B.3 plus a deferred-status spec for the codex `Redactor` utility, all under `docs/superpowers/`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Layout: components-outer with game suffix.** Every game's code lives at `IndifferentKetchup\Codex\<Component>\<Game>\*` for the existing components (`Analyser`, `Analysis`, `Detective`, `Log`, `Parser`, `Pattern`). This is option 1 from the Phase A Step 2 layout decision; option 3 (a flat `IndifferentKetchup\Codex\Games\<Game>\*` tree) was originally proposed and was **not** selected.
|
||||||
|
- **`LICENSE`** retains the original `Copyright (c) 2019-2026 Aternos GmbH` line per MIT requirements; the LICENSE file is byte-for-byte unchanged from the upstream import.
|
||||||
|
- **`composer.json`** rewritten in `aae016d`: package name `indifferentketchup/codex`, MIT license, generic-framework description, single author entry, PSR-4 autoload roots set to `IndifferentKetchup\Codex\` and the test-fixture / test-suite namespaces, PHP `>=8.4` require constraint, PHPUnit `^12` dev dependency.
|
||||||
|
- **`tests.yaml`** uses the modern `$GITHUB_OUTPUT` workflow command instead of the deprecated `::set-output` (commit `60f12bc`). CI matrix runs PHP 8.4 and 8.5.
|
||||||
|
- **`.gitignore`** excludes `Logs.zip` (real production log fixtures) and `.scratch/` (extracted reference logs), plus `.claude/` and `.claude.local.md` for personal Claude Code artefacts.
|
||||||
|
|
||||||
|
### Deferred
|
||||||
|
|
||||||
|
- **Codex `Redactor` utility** — design captured in `docs/superpowers/specs/2026-04-30-redactor-design.md`. Not implemented in v0.1.0. iblogs (the downstream consumer) handles upload-time PII filtering for this release; codex itself ships no PII helper. The deferred spec exists so iblogs's privacy story has a referenced design to point at and so a future implementation pass has a clear contract to start from.
|
||||||
|
- **Other game implementations** — `Minecraft`, `Hytale`, and `SevenDaysToDie` are detective-stub-only. Each has a TODO `<Game>Detective` extending base `Detective`; their per-component subdirectories under `Analyser`, `Log`, `Parser`, and `Pattern` contain only `.gitkeep` placeholders. Real implementations land if and when fixtures and demand exist.
|
||||||
|
- **Packagist publication** — v0.1.0 is consumable via Composer's `vcs` repository entry pointing at the Gitea remote. Pushing to Packagist is a separate decision and is not in scope for this release.
|
||||||
|
|
||||||
|
[0.1.0]: https://git.indifferentketchup.com/indifferentketchup/ik-codex/releases/tag/v0.1.0
|
||||||
99
CLAUDE.md
Normal file
99
CLAUDE.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
`indifferentketchup/codex` — a generic PHP log parsing and analysis framework, plus per-game subclasses that adapt the framework to specific games' log formats. PHP `>=8.4`, MIT license. Forked from `aternos/codex`; namespace was renamed in-tree (`Aternos\Codex` → `IndifferentKetchup\Codex`) — only the LICENSE retains the original Aternos GmbH copyright line, which must remain byte-for-byte (MIT requires it).
|
||||||
|
|
||||||
|
## Local environment
|
||||||
|
|
||||||
|
PHP and Composer are **not** installed on the host. All Composer/PHPUnit invocations go through the official `composer:latest` Docker image (currently PHP 8.5, satisfies the `>=8.4` floor):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest <subcommand>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `$(pwd)` or an absolute path — bare `$PWD` has misfired here, mounting nothing and silently no-op'ing the run.
|
||||||
|
|
||||||
|
## Common commands
|
||||||
|
|
||||||
|
- All tests: `composer test` (= `phpunit test/tests` per `composer.json`)
|
||||||
|
- One test file or method (wrap in the same docker invocation):
|
||||||
|
`docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit --filter=testFooBar test/tests/path/to/SomeTest.php`
|
||||||
|
- Refresh autoloader after editing `composer.json`: `composer dump-autoload`
|
||||||
|
- After cloning: `composer install` (writes `vendor/`, gitignored)
|
||||||
|
|
||||||
|
## Framework architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
LogFile (Path|String|Stream)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Log ── extends AnalysableLog ── implements DetectableLogInterface
|
||||||
|
│ │ │
|
||||||
|
│ │ └─ static getDetectors(): Detector[]
|
||||||
|
│ └─ static getDefaultAnalyser(): Analyser
|
||||||
|
├─ static getDefaultParser(): Parser
|
||||||
|
│
|
||||||
|
▼ Log->parse()
|
||||||
|
Entry[] of Line[] (each Entry has level, time, prefix, lines)
|
||||||
|
│
|
||||||
|
▼ Log->analyse()
|
||||||
|
Analysis of Insight[]
|
||||||
|
└── Information (label + value) or
|
||||||
|
Problem (with attached Solution[])
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Detective`** ranks candidate Log subclasses by running each candidate's `getDetectors()` and picking the highest-scoring result (`bool|float`). It receives a `LogFile`, returns a constructed `Log` subclass.
|
||||||
|
- **`PatternParser`** is regex-driven. Lines that don't match the LINE regex append to the previous `Entry` — this is the mechanism that handles multi-line records like Java stack traces under an ERROR header.
|
||||||
|
- **`PatternAnalyser`** walks entries, runs each registered insight class's static `getPatterns()` against entry text via `preg_match_all`, and emits coalesced insights (equal insights bump a counter instead of duplicating).
|
||||||
|
- **Custom `Analyser` subclasses** are the right move when analysis needs cross-entry state — pairing events, sliding-window thresholds, comparing consecutive snapshots. `PatternAnalyser` operates per-entry only and can't express those. Phase B.3 (`ConnectionFailureAnalyser`, `ItemDuplicationAnalyser`, `SkillProgressionAnomalyAnalyser`) shows the shape: extend `Analyser`, override `analyse()`, walk `$this->log` once, aggregate, then emit coalesced `Problem`/`Information` insights at the end. Tunable thresholds belong as `public const` constants on the subclass with the rationale in a docblock.
|
||||||
|
- Detectors available out of the box: `SinglePatternDetector`, `WeightedSinglePatternDetector`, `LinePatternDetector` (returns match ratio), `MultiPatternDetector` (AND), and the path-based `FilenameDetector` (uses `LogFileInterface::getPath()`, returns `false` when no path is available).
|
||||||
|
|
||||||
|
## Game subtrees
|
||||||
|
|
||||||
|
Layout is **components-outer with game suffix**, not games-outer:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/<Component>/<Game>/... e.g. src/Log/ProjectZomboid/ProjectZomboidServerLog.php
|
||||||
|
src/Pattern/<Game>/<Type>Pattern.php (regex string constants; not a framework abstraction)
|
||||||
|
test/tests/Games/<Game>/...
|
||||||
|
test/src/Games/<Game>/fixtures/<type>-minimal.txt (synthetic fixtures only)
|
||||||
|
```
|
||||||
|
|
||||||
|
Scaffolded games: `Minecraft`, `Hytale`, `SevenDaysToDie` (stubs only — empty `.gitkeep`s plus a TODO `<Game>Detective` extending base `Detective`). `ProjectZomboid` is fully implemented: 11 log subclasses, 11 pattern classes, detective wired with all 11, synthetic fixtures, dispatch tests, plus the analyser surface — 12 `PatternAnalyser`-driven Insight classes under `src/Analysis/ProjectZomboid/` and 3 custom `Analyser` subclasses under `src/Analyser/ProjectZomboid/` for cross-entry / threshold logic.
|
||||||
|
|
||||||
|
`src/Pattern/` is **not a framework abstraction** — patterns are plain `string` class constants. Each `<Type>Pattern` typically holds a `LINE` constant for the parser plus named-group extractor constants (`FIELDS`, `COMBAT`, `MOD_LOAD`, etc.) for analysers.
|
||||||
|
|
||||||
|
### ProjectZomboid specifics
|
||||||
|
|
||||||
|
- Two abstract bases: `ProjectZomboidLog` (`TIME_FORMAT = 'd-m-y H:i:s.v'`, UTC default, `makePatternParser()` helper) and `ProjectZomboidEventLog` (marker for the ten single-line logs; `ProjectZomboidServerLog` extends the parent directly because it permits multi-line entries).
|
||||||
|
- `ProjectZomboidDetective::__construct()` pre-registers all 11 log classes — instantiate it and call `setLogFile(...)->detect()`.
|
||||||
|
- Each Log subclass's `getDefaultAnalyser()` returns one of:
|
||||||
|
- A custom `Analyser` subclass (cross-entry logic): `UserLog → ConnectionFailureAnalyser`, `ItemLog → ItemDuplicationAnalyser`, `PerkLog → SkillProgressionAnomalyAnalyser`.
|
||||||
|
- A configured `PatternAnalyser` (per-entry pattern matching): `ServerLog`, `PvpLog`, `AdminLog` register their respective Insight classes.
|
||||||
|
- An empty `PatternAnalyser` for logs with no analysers yet: `ChatLog`, `ClientActionLog`, `CmdLog`, `MapLog`, `BurdJournalsLog`. These are wiring stubs awaiting future analysis work.
|
||||||
|
|
||||||
|
### Standard test template for a Log subclass
|
||||||
|
|
||||||
|
At minimum: (1) entry count after `parse()` matches the synthetic fixture's line count, (2) one or more named-group `FIELDS` regexes from the `<Type>Pattern` class extract correctly from a representative line, (3) `Detective` handed the fixture path returns an instance of this Log class. Use `#[DataProvider]` when the same shape repeats per file.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
1. **`PatternParser` is incompatible with named regex groups.** PHP's `preg_match` returns named groups *plus* their numeric duplicates in the same array; `PatternParser`'s foreach iterates both and throws on the string-key entries. Convention: `LINE` regexes (used by the parser) use **unnamed** groups with field order documented in the Pattern class's docblock. Named groups are fine inside extractor regexes invoked from analysers, since `PatternAnalyser` hands the whole match array to `Insight::setMatches`.
|
||||||
|
2. **PHPUnit 12 requires the `#[DataProvider('methodName')]` attribute.** The legacy `@dataProvider` annotation silently passes zero args and fails with `ArgumentCountError`.
|
||||||
|
3. **`Level::fromString()` defaults to `Level::INFO` for unknown tokens.** Project Zomboid log levels map: `LOG`/`INFO` → INFO; `WARN` → WARNING; `ERROR` → ERROR.
|
||||||
|
4. **`PatternParser` matches array** must declare a match-type for **every** capture group in the regex (`TIME`, `LEVEL`, or `PREFIX`); otherwise the parser throws on the unmapped index. Use non-capturing groups `(?:...)` for fields you want to skip.
|
||||||
|
|
||||||
|
## Workflow conventions
|
||||||
|
|
||||||
|
- **One commit per concrete log type** when adding game support: pattern class + log subclass + synthetic fixture + test in a single commit, run `composer test`, then move on. `<Game>Detective::__construct()` wiring goes in its own follow-up commit once all log types are present.
|
||||||
|
- **Out-of-scope cleanup goes in its own commit.** Tempting workflow/lint fixes (e.g. deprecated CI syntax, comment hygiene) noticed mid-feature should not be folded in — separate commit or follow-up PR.
|
||||||
|
- **Pre-destructive checkpoint pattern.** Before bulk renames/moves: `git commit --allow-empty -m "pre-X checkpoint"` as a revert anchor. Skip the empty slot if it produces no diff at the end of a plan.
|
||||||
|
|
||||||
|
## Privacy / fixture rules
|
||||||
|
|
||||||
|
- `Logs.zip` at the repo root contains real production server data (Steam IDs, player names, world coordinates). It is gitignored.
|
||||||
|
- Extract for reference: `unzip -q Logs.zip -d .scratch/pz/`. Real logs then live under `.scratch/pz/Logs/` (gitignored). Use only as format reference. Do not paste raw Steam IDs, player names, or coordinates into chat output, commit messages, or any committed file.
|
||||||
|
- All fixtures committed under `test/src/Games/<Game>/fixtures/` must be **synthetic**, hand-crafted from the observed format with placeholder identifiers: `76561198000000001/2/3` for Steam IDs, `Player1`/`Player2`/`AdminUser` for names, generic coords (`1000-1100, 2000-2200, 0`).
|
||||||
168
README.md
168
README.md
@@ -1,115 +1,97 @@
|
|||||||
# Codex
|
# IndifferentKetchup Codex
|
||||||
|
|
||||||
### About
|
Generic PHP log parsing and analysis framework. Reads a log file, detects which log type it is, parses entries (including multi-line records like Java stack traces), runs the type-specific analysers, and returns structured `Information` and `Problem` insights with attached `Solution`s where applicable.
|
||||||
|
|
||||||
Codex (*lat. roughly for "log"*) is a PHP library to read, parse, print and analyse log files to find problems and suggest possible
|
Originally a fork of [`aternos/codex`](https://github.com/aternosorg/codex); the framework is intentionally game-agnostic. The reference implementation in this tree is Project Zomboid server logs.
|
||||||
solutions. It was created mainly for Minecraft server logs but could be used for any other logs as well. This library provides a set
|
|
||||||
up for a structured log parsing implementation and provides some useful basic implementation, mainly based on RegEx. Every part
|
|
||||||
of this library can or even must be extended/overwritten while still following the interfaces, which ensure interoperability between
|
|
||||||
the different parts of this library.
|
|
||||||
|
|
||||||
### Installation
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
composer require aternos/codex
|
composer require indifferentketchup/codex
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
Requires PHP `>=8.4`. No third-party runtime dependencies.
|
||||||
|
|
||||||
This is a short introduction to the idea of Codex, for some more examples check the [test](test) folder
|
## Quick start
|
||||||
and/or read the [code](src).
|
|
||||||
|
|
||||||
### Logfile
|
Given a Project Zomboid `DebugLog-server.txt`:
|
||||||
|
|
||||||
A [`LogFile`](src/Log/File/LogFile.php) object implementing the [`LogFileInterface`](src/Log/File/LogFileInterface.php) object is required
|
|
||||||
to start reading a log. There are currently three different log file classes in this library.
|
|
||||||
|
|
||||||
```php
|
```php
|
||||||
<?php
|
<?php
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
$logFile = new \Aternos\Codex\Log\File\StringLogFile("This is the log content");
|
use IndifferentKetchup\Codex\Detective\ProjectZomboid\ProjectZomboidDetective;
|
||||||
$logFile = new \Aternos\Codex\Log\File\PathLogFile("/path/to/log");
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
$logFile = new \Aternos\Codex\Log\File\StreamLogFile(fopen("/path/to/log", "r"));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Log
|
$detective = new ProjectZomboidDetective();
|
||||||
|
$detective->setLogFile(new PathLogFile('2026-04-30_14-00_DebugLog-server.txt'));
|
||||||
|
|
||||||
A [`Log`](src/Log/Log.php) object implementing the [`LogInterface`](src/Log/LogInterface.php) is the most important object
|
|
||||||
for the different operations. It represents the log content, which is split in [Entries](src/Log/EntryInterface.php) and [Lines](src/Log/LineInterface.php).
|
|
||||||
And it offers quick access to the detection, parsing and analysing functions and can define which classes are used
|
|
||||||
for those functions. If you know which log type you have or just want to test the default [Log](src/Log/Log.php) class, you can
|
|
||||||
directly create a new instance, otherwise you can use detection as described below.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$log = new \Aternos\Codex\Log\Log();
|
|
||||||
$log->setLogFile($logFile);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Detection
|
|
||||||
|
|
||||||
If the log type (specifically the class name of the log type) is unknown you can use the [`Detective`](src/Detective/Detective.php) class
|
|
||||||
to automatically detect the log type. The `Detective` class gets a list of possible log class names and executes
|
|
||||||
their given [Detectors](src/Detective/DetectorInterface.php).
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$detective = new \Aternos\Codex\Detective\Detective();
|
|
||||||
$detective->addPossibleLogClass(\Aternos\Codex\Log\Log::class);
|
|
||||||
$log = $detective->detect();
|
$log = $detective->detect();
|
||||||
```
|
|
||||||
|
|
||||||
The `detect()` function always returns a log object, if necessary it defaults to [`Log`](src/Log/Log.php).
|
|
||||||
|
|
||||||
### Parsing
|
|
||||||
|
|
||||||
Parsing reads the entire log and creates the [`Entry`](src/Log/EntryInterface.php) and [`Line`](src/Log/LineInterface.php) objects which
|
|
||||||
are parts of a [`Log`](src/Log/LogInterface.php) object. Different log types can use different parsers by overwriting the
|
|
||||||
`LogInterface::getDefaultParser()` function or by passing a parser object to the parse function.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$log->parse();
|
$log->parse();
|
||||||
```
|
|
||||||
|
|
||||||
### Analysing
|
|
||||||
|
|
||||||
An analysis is performed by an [`Analyser`](src/Analyser/AnalyserInterface.php) on an [`AnalysableLog`](src/Log/AnalysableLogInterface.php) and returns
|
|
||||||
an [`Analysis`](src/Analysis/AnalysisInterface.php) object containing various [`Insight`](src/Analysis/InsightInterface.php) objects, e.g. a [`Problem`](src/Analysis/ProblemInterface.php)
|
|
||||||
or an [`Information`](src/Analysis/InformationInterface.php) object. Different log types can use different analysers by overwriting
|
|
||||||
the `AnalysableLogInterface::getDefaultAnalyser()` function or by passing an analyser object to the analyse function.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$analysis = $log->analyse();
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
echo $log->getTitle(), "\n\n";
|
||||||
|
|
||||||
|
foreach ($analysis->getInformation() as $info) {
|
||||||
|
echo "[INFO] ", $info->getMessage(), "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($analysis->getProblems() as $problem) {
|
||||||
|
echo "[PROBLEM] ", $problem->getMessage(), "\n";
|
||||||
|
foreach ($problem->getSolutions() as $solution) {
|
||||||
|
echo " -> ", $solution->getMessage(), "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Printing
|
For a session with mod issues and a server-side exception, output looks roughly like:
|
||||||
|
|
||||||
The entire [`Log`](src/Log/LogInterface.php) or just an [`Entry`](src/Log/EntryInterface.php) can be printed through a [`Printer`](src/Printer/PrinterInterface.php). The basic
|
|
||||||
[`DefaultPrinter`](src/Printer/DefaultPrinter.php) only prints the plain content line by line. The [`ModifiableDefaultPrinter`](src/Printer/ModifiableDefaultPrinter.php)
|
|
||||||
allows [`Modification`](src/Printer/ModificationInterface.php), e.g. to highlight certain characters/words.
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
|
|
||||||
$printer = new \Aternos\Codex\Printer\DefaultPrinter();
|
|
||||||
$printer->setLog($log);
|
|
||||||
$printer->print();
|
|
||||||
|
|
||||||
$printer = new \Aternos\Codex\Printer\DefaultPrinter();
|
|
||||||
$printer->setEntry($entry);
|
|
||||||
$printer->print();
|
|
||||||
|
|
||||||
$printer = new \Aternos\Codex\Printer\ModifiableDefaultPrinter();
|
|
||||||
$printer->setLog($log);
|
|
||||||
$modification = new \Aternos\Codex\Printer\PatternModification();
|
|
||||||
$modification->setPattern('/foo/');
|
|
||||||
$modification->setReplacement('bar');
|
|
||||||
$printer->addModification($modification);
|
|
||||||
$printer->print();
|
|
||||||
```
|
```
|
||||||
|
Project Zomboid Debug Server Log
|
||||||
|
|
||||||
|
[INFO] Engine version: 42.16.3 (build <hash>, <build date>)
|
||||||
|
[INFO] Mod loaded: <mod_id>
|
||||||
|
[INFO] Mod loaded: <other_mod_id>
|
||||||
|
[PROBLEM] Required mod "<missing>" not found.
|
||||||
|
-> Subscribe to mod "<missing>" or remove its ID from the Mods= line in serverconfig.ini.
|
||||||
|
[PROBLEM] Exception thrown: java.nio.file.NoSuchFileException
|
||||||
|
```
|
||||||
|
|
||||||
|
If the log content arrives without a filesystem path (clipboard paste, web upload, stream), use `StringLogFile` or `StreamLogFile` instead of `PathLogFile`. The detective falls back to content signatures when the filename hint is absent.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
LogFile → Log → parse() → Entry[] of Line[] → analyse() → Analysis of Insight[]
|
||||||
|
└── Information | Problem(+Solutions)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`Detective`** ranks candidate `Log` subclasses by running each candidate's static `getDetectors()` and picking the highest-scoring result. Each game ships its own `<Game>Detective` that pre-registers its log classes.
|
||||||
|
- **`PatternParser`** is regex-driven; lines that don't match the entry-start regex append to the previous `Entry`, which is how multi-line records (Java stack traces, indented warnings) are kept intact.
|
||||||
|
- **Analysers** come in two flavours: configured `PatternAnalyser` instances for per-entry pattern matching, and custom subclasses of `Analyser` for cross-entry logic (pairing events, sliding-window thresholds, snapshot comparisons).
|
||||||
|
- **Insights** are either `Information` (label + value) or `Problem` (with attached `Solution`s). Equal insights coalesce via a counter, so repeated patterns don't produce duplicate output.
|
||||||
|
|
||||||
|
Patterns live as plain `string` constants under `src/Pattern/<Game>/` — there is no `PatternInterface`. Each game adds files under `src/<Component>/<Game>/` (components-outer, game-suffixed). Full extension guide and conventions in [`CLAUDE.md`](CLAUDE.md).
|
||||||
|
|
||||||
|
## Game support
|
||||||
|
|
||||||
|
| Game | State |
|
||||||
|
|---|---|
|
||||||
|
| Project Zomboid | Full: 11 log subclasses across all the file types a server emits; analysers covering engine version, mod loading, server exceptions, PvP combat, admin audit, connection failures, item duplication, skill progression anomalies |
|
||||||
|
| Minecraft | Stub only — `MinecraftDetective` skeleton, no log subclasses yet |
|
||||||
|
| Hytale | Stub only |
|
||||||
|
| Seven Days To Die | Stub only |
|
||||||
|
|
||||||
|
The framework itself is generic — adding a new game means writing the same shape of files Project Zomboid demonstrates, not modifying anything in `src/{Analyser,Analysis,Detective,Log,Parser,Printer,Pattern}/` outside the new game's subdirectory.
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
`composer test` runs the suite. PHP and Composer are not required on the host — invocations wrap in the official `composer:latest` Docker image (PHP 8.5). See [`CLAUDE.md`](CLAUDE.md) for the wrapped command, file layout, and the workflow conventions used in this repo.
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
<https://git.indifferentketchup.com/indifferentketchup/ik-codex>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [`LICENSE`](LICENSE).
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aternos/codex",
|
"name": "indifferentketchup/codex",
|
||||||
"description": "PHP library to read, parse, print and analyse log files.",
|
"description": "Generic PHP log parsing and analysis framework.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Matthias Neid",
|
"name": "indifferentketchup",
|
||||||
"email": "matthias@aternos.org"
|
"email": "samkintop@gmail.com"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Aternos\\Codex\\": "src/"
|
"IndifferentKetchup\\Codex\\": "src/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"Aternos\\Codex\\Test\\Src\\": "test/src/",
|
"IndifferentKetchup\\Codex\\Test\\Src\\": "test/src/",
|
||||||
"Aternos\\Codex\\Test\\Tests\\": "test/tests/"
|
"IndifferentKetchup\\Codex\\Test\\Tests\\": "test/tests/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
74
docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md
Normal file
74
docs/superpowers/plans/2026-04-30-pz-analysers-deferred.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# ProjectZomboid Phase B.3 Deferred Analysers — As-Built Plan
|
||||||
|
|
||||||
|
> Retroactive: written 2026-05-01.
|
||||||
|
|
||||||
|
This document is a historical record of how Phase B.3 (the three deferred analysers from the original Step D candidate list) was implemented. The corresponding design spec is `docs/superpowers/specs/2026-04-30-pz-analysers-deferred-design.md`. The work is complete and merged to `master`; checkboxes are pre-checked.
|
||||||
|
|
||||||
|
**Goal:** Land three custom `Analyser` subclasses under `src/Analyser/ProjectZomboid/` (the first non-empty contents of that directory), three `Problem` subclasses under `src/Analysis/ProjectZomboid/`, threshold constants documented inline as `public const`, fixture extensions to exercise trigger and non-trigger paths, and e2e tests verifying the analysers' behaviour against the fixtures.
|
||||||
|
|
||||||
|
**Architecture:** Custom subclasses of the framework's abstract `Analyser`. Each overrides `analyse()` to walk `$this->log` once, aggregate cross-entry state, and emit coalesced `Problem` insights at the end. This is the first deviation from Phase B.1/B.2's vanilla-`PatternAnalyser` pattern; the reasoning is recorded in the design spec and in `CLAUDE.md`.
|
||||||
|
|
||||||
|
**Tech Stack:** PHP 8.4+, PHPUnit 12, Composer (root package: `indifferentketchup/codex`). PHP/Composer not installed on host — all command invocations wrap in `docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest …`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Task 0 — Pre-checkpoint
|
||||||
|
|
||||||
|
- [x] Empty checkpoint commit: `c444e85 pre-phase-B.3 checkpoint`
|
||||||
|
|
||||||
|
### Task 1 — `ConnectionFailureAnalyser` (UserLog)
|
||||||
|
|
||||||
|
Pairing logic: walk the log, count `attempting to join` and `allowed to join` events per Steam ID, emit a `ConnectionFailureProblem` for any Steam ID whose attempt count exceeds its allowed count.
|
||||||
|
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/ConnectionFailureProblem.php` (Steam ID, player, unmatched count; `isEqual` coalesces by Steam ID)
|
||||||
|
- [x] Add `src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php` — first file in this directory; the `.gitkeep` placeholder is removed in this commit
|
||||||
|
- [x] Wire `ProjectZomboidUserLog::getDefaultAnalyser()` to return `new ConnectionFailureAnalyser()` and drop the now-unused `PatternAnalyser` import
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analyser/UserLogAnalysisTest.php` — asserts Player1 (`76561198000000001`) is flagged with `unmatchedAttempts == 1` and Player2 (`76561198000000002`) is not flagged
|
||||||
|
- [x] `composer test` green: 188 tests, 392 assertions
|
||||||
|
- [x] Commit: `73e9ca6 Add ConnectionFailureAnalyser`
|
||||||
|
|
||||||
|
Design note inside the analyser docblock: "attempting to join used queue" rows are surfaced as failures in v1 because a long queue wait is indistinguishable from a real failure without timing context. Tunable in v2 if false positives become noisy.
|
||||||
|
|
||||||
|
### Task 2 — `ItemDuplicationAnalyser` (ItemLog)
|
||||||
|
|
||||||
|
Sliding-window heuristic over `(steamid, item)` groups, restricted to positive-delta events. Negative-delta rows (drops/transfers) are filtered out.
|
||||||
|
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/ItemDuplicationProblem.php` (Steam ID, player, item, event count; `isEqual` coalesces by `(steamid, item)`)
|
||||||
|
- [x] Add `src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php` with two threshold constants and rationale docblocks: `THRESHOLD_COUNT = 5`, `THRESHOLD_WINDOW_SECONDS = 10`
|
||||||
|
- [x] Wire `ProjectZomboidItemLog::getDefaultAnalyser()` to return `new ItemDuplicationAnalyser()`; drop unused `PatternAnalyser` import
|
||||||
|
- [x] Extend `test/src/Games/ProjectZomboid/fixtures/item-minimal.txt`: append 6 Bullets9mm events at sub-second timestamps `19:50:00.001`–`.006` for AdminUser (trigger), plus 4 Plank events scattered `20:00:00`–`20:03:00` for Player1 (sub-threshold)
|
||||||
|
- [x] Bump entry-count assertion in `ProjectZomboidItemLogTest::testParsesEachLineAsAnEntry`: 10 → 20
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analyser/ItemLogAnalysisTest.php` — asserts one `ItemDuplicationProblem` (AdminUser + Bullets9mm + 6 events), zero for the Plank group, and the threshold constants are positive
|
||||||
|
- [x] `composer test` green: 191 tests, 400 assertions
|
||||||
|
- [x] Commit: `ba3fae8 Add ItemDuplicationAnalyser`
|
||||||
|
|
||||||
|
Implementation note: the analyser uses a two-pointer sliding window per group, which is O(n) per group after the initial sort. `Entry::getTime()` returns integer Unix seconds (sub-second precision dropped); the burst events all collapse to the same Unix-second value so any positive window catches them.
|
||||||
|
|
||||||
|
### Task 3 — `SkillProgressionAnomalyAnalyser` (PerkLog)
|
||||||
|
|
||||||
|
Compare consecutive perks-snapshot rows per Steam ID; emit a problem for any single skill that gained more than `THRESHOLD_DELTA` levels between snapshots.
|
||||||
|
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/SkillProgressionAnomalyProblem.php` (Steam ID, player, skill, fromLevel, toLevel, delta; `isEqual` coalesces by `(steamid, skill)`)
|
||||||
|
- [x] Add `src/Analyser/ProjectZomboid/SkillProgressionAnomalyAnalyser.php` with `THRESHOLD_DELTA = 3` and a rationale docblock about PZ's slow skill leveling
|
||||||
|
- [x] Wire `ProjectZomboidPerkLog::getDefaultAnalyser()` to return `new SkillProgressionAnomalyAnalyser()`; drop unused `PatternAnalyser` import
|
||||||
|
- [x] Extend `test/src/Games/ProjectZomboid/fixtures/perk-minimal.txt`: append PlayerSuspect (Steam ID `76561198000000004`) with two snapshots — Strength 2→10 (+8 trigger), Fitness 2→8 (+6 trigger), Maintenance 0→3 (+3 boundary, does not trigger because comparison is strict `>`)
|
||||||
|
- [x] Bump entry-count assertion in `ProjectZomboidPerkLogTest::testParsesEachLineAsAnEntry`: 6 → 10
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analyser/PerkLogAnalysisTest.php` — asserts exactly two problems for PlayerSuspect (Strength + Fitness, sorted), no problem for Maintenance, no problems for single-snapshot Player1/Player2, and the threshold constant is positive
|
||||||
|
- [x] `composer test` green: 195 tests, 412 assertions
|
||||||
|
- [x] Commit: `0c90e40 Add SkillProgressionAnomalyAnalyser`
|
||||||
|
|
||||||
|
Filtering note: the analyser skips event-token rows (`Login`, `Logout`, `LevelUp`) by checking that the bracketed event field contains a `Skill=N` pair via `PerkPattern::PERK_PAIR`. Only true perks-snapshot rows enter the comparison.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done condition (met)
|
||||||
|
|
||||||
|
After Task 3, `composer test` reports **195 tests, 412 assertions, all green** under PHPUnit 12.5.6 / PHP 8.5.5. All eight Step D candidate analysers (Phase B.1's three ServerLog + Phase B.2's seven PvP/Admin + Phase B.3's three deferred) are operational across their respective Log subclasses.
|
||||||
|
|
||||||
|
The directory `src/Analyser/ProjectZomboid/` now contains real code for the first time; its `.gitkeep` placeholder was removed in `73e9ca6`.
|
||||||
|
|
||||||
|
## Deviations from the original plan
|
||||||
|
|
||||||
|
None this phase. The 4-commit count and the per-analyser shape both match what was committed-to in chat before execution. No silent breakages, no missing closing braces. The only observation worth recording is that the planned commit count was inclusive of the pre-checkpoint, and the actual commit ordering matched the plan exactly.
|
||||||
116
docs/superpowers/plans/2026-04-30-pz-analysers-pvp-admin.md
Normal file
116
docs/superpowers/plans/2026-04-30-pz-analysers-pvp-admin.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# ProjectZomboid Phase B.2 Analysers — As-Built Plan
|
||||||
|
|
||||||
|
> Retroactive: written 2026-05-01.
|
||||||
|
|
||||||
|
This document is a historical record of how Phase B.2 (PvP combat detection + admin verb dispatch) was implemented. The corresponding design spec is `docs/superpowers/specs/2026-04-30-pz-analysers-pvp-admin-design.md`. The work is complete and merged to `master`; checkboxes are pre-checked.
|
||||||
|
|
||||||
|
**Goal:** Land seven new `Information` insight classes (one for PvP combat, six for admin verbs) under `src/Analysis/ProjectZomboid/`, plus seven new pattern constants on `PvpPattern` / `AdminPattern`, then wire `ProjectZomboidPvpLog` and `ProjectZomboidAdminLog` default analysers to register them.
|
||||||
|
|
||||||
|
**Architecture:** Vanilla `PatternAnalyser` configured with the new insight classes. No custom `Analyser` subclasses (deferred to Phase B.3). `Entry::__toString()` joins lines with `\n`, but B.2 logs are single-line per entry so multi-line behaviour doesn't apply here.
|
||||||
|
|
||||||
|
**Tech Stack:** PHP 8.4+, PHPUnit 12, Composer (root package: `indifferentketchup/codex`). PHP/Composer not installed on host — all command invocations wrap in `docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest …`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Task 0 — Pre-checkpoint
|
||||||
|
|
||||||
|
- [x] Empty checkpoint commit: `df62da1 pre-phase-B.2 checkpoint`
|
||||||
|
|
||||||
|
### Task 1 — `PvpDamageInformation` + `PvpPattern::COMBAT_REAL`
|
||||||
|
|
||||||
|
- [x] Add `PvpPattern::COMBAT_REAL` constant (combat regex with negative lookahead on weapon and positive-non-zero damage clause)
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/PvpDamageInformation.php`
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analysis/PvpDamageInformationTest.php` covering pattern shape, match extraction, and three rejection cases (zombie weapon, zero damage, negative damage)
|
||||||
|
- [x] `composer test` green: 167 tests, 343 assertions
|
||||||
|
- [x] Commit: `55f769c Add PvpDamageInformation insight`
|
||||||
|
|
||||||
|
### Task 2 — `AdminAddedItemInformation` + `AdminPattern::ADDED_ITEM_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::ADDED_ITEM_ENTRY` constant (entry-anchored variant; the body-only `ADDED_ITEM` from Phase A stays in place)
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminAddedItemInformation.php`
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analysis/AdminAddedItemInformationTest.php`
|
||||||
|
- [x] Commit: `90c85a0 Add AdminAddedItemInformation insight` — **see Deviations section below**
|
||||||
|
- [x] Forward-fix: `0d85a05 Fix missing closing brace in AdminPattern`
|
||||||
|
- [x] `composer test` green after forward-fix: 170 tests
|
||||||
|
|
||||||
|
### Task 3 — `AdminAddedXpInformation` + `ADDED_XP_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::ADDED_XP_ENTRY` constant
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminAddedXpInformation.php`
|
||||||
|
- [x] Unit test
|
||||||
|
- [x] `composer test` green: 173 tests
|
||||||
|
- [x] Commit: `a2faa55 Add AdminAddedXpInformation insight`
|
||||||
|
|
||||||
|
### Task 4 — `AdminGrantedAccessInformation` + `GRANTED_ACCESS_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::GRANTED_ACCESS_ENTRY` constant
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminGrantedAccessInformation.php`
|
||||||
|
- [x] Unit test
|
||||||
|
- [x] `composer test` green: 175 tests
|
||||||
|
- [x] Commit: `caed04d Add AdminGrantedAccessInformation insight`
|
||||||
|
|
||||||
|
### Task 5 — `AdminChangedOptionInformation` + `CHANGED_OPTION_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::CHANGED_OPTION_ENTRY` constant
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminChangedOptionInformation.php`
|
||||||
|
- [x] Unit test
|
||||||
|
- [x] `composer test` green: 177 tests
|
||||||
|
- [x] Commit: `b7b89ef Add AdminChangedOptionInformation insight`
|
||||||
|
|
||||||
|
### Task 6 — `AdminReloadedOptionsInformation` + `RELOADED_OPTIONS_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::RELOADED_OPTIONS_ENTRY` constant
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminReloadedOptionsInformation.php`
|
||||||
|
- [x] Unit test
|
||||||
|
- [x] `composer test` green: 179 tests
|
||||||
|
- [x] Commit: `64641fa Add AdminReloadedOptionsInformation insight`
|
||||||
|
|
||||||
|
### Task 7 — `AdminTeleportedInformation` + `TELEPORTED_ENTRY`
|
||||||
|
|
||||||
|
- [x] Add `AdminPattern::TELEPORTED_ENTRY` constant (handles negative Z for basement coordinates)
|
||||||
|
- [x] Add `src/Analysis/ProjectZomboid/AdminTeleportedInformation.php`
|
||||||
|
- [x] Unit test (positive and negative Z cases)
|
||||||
|
- [x] `composer test` green: 182 tests
|
||||||
|
- [x] Commit: `d15fc81 Add AdminTeleportedInformation insight`
|
||||||
|
|
||||||
|
### Task 8 — Wire `ProjectZomboidPvpLog::getDefaultAnalyser()`
|
||||||
|
|
||||||
|
- [x] Replace `return new PatternAnalyser();` with `(new PatternAnalyser())->addPossibleInsightClass(PvpDamageInformation::class)`
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analyser/PvpLogAnalysisTest.php` — asserts three real-PvP insights (Bare Hands, Tire Iron, Hunting Knife) and zero zombie/vehicle insights
|
||||||
|
- [x] `composer test` green: 184 tests
|
||||||
|
- [x] Commit: `51eb2de Wire ProjectZomboidPvpLog default analyser`
|
||||||
|
|
||||||
|
### Task 9 — Wire `ProjectZomboidAdminLog::getDefaultAnalyser()`
|
||||||
|
|
||||||
|
- [x] Register all six `Admin<Verb>Information` classes
|
||||||
|
- [x] Add `test/tests/Games/ProjectZomboid/Analyser/AdminLogAnalysisTest.php` — asserts the 2+2+2+2+1+2 distribution and confirms the duplicate ShotgunShells row coalesces with `counter == 2`
|
||||||
|
- [x] `composer test` green: 186 tests
|
||||||
|
- [x] Commit: `c57d646 Wire ProjectZomboidAdminLog default analyser`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deviations from the original plan
|
||||||
|
|
||||||
|
### The `90c85a0` brace-fix interlude
|
||||||
|
|
||||||
|
Task 2's commit (`90c85a0 Add AdminAddedItemInformation insight`) shipped broken. While adding the first `_ENTRY` constant to `AdminPattern.php`, the `Edit` tool's `old_string` was `<TELEPORTED line>\n}` and the `new_string` included a docblock plus the new constant but **dropped the closing brace** of the class body. The commit was made before the test result was inspected, so it landed with a `ParseError: Unclosed '{'` and 9 cascading test errors.
|
||||||
|
|
||||||
|
Forward-fix `0d85a05 Fix missing closing brace in AdminPattern` restored the brace as a separate commit (per the `CLAUDE.md` workflow rule: "Always create new commits rather than amending"). The broken intermediate commit remains in history; force-pushing master to clean it would have cost more than the cosmetic gain.
|
||||||
|
|
||||||
|
The remaining five admin commits (Tasks 3–7) used a deliberate practice change: every subsequent `Edit` to `AdminPattern.php` included the closing `}` in both `old_string` and `new_string` so it couldn't be dropped again. No further breakage.
|
||||||
|
|
||||||
|
### Total commit count
|
||||||
|
|
||||||
|
11 commits vs the 10 originally outlined in the spec's planning section. The extra commit is the brace-fix.
|
||||||
|
|
||||||
|
### Test-count divergence note (now resolved)
|
||||||
|
|
||||||
|
When Phase B.1's plan was written I projected a final count of 158 tests for B.1; the actual landed count was 161 (off by 3 — Task 5's contribution wasn't summed in the plan footer). For B.2 the planned and actual per-step counts match exactly. No projection error this phase.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done condition (met)
|
||||||
|
|
||||||
|
After Task 9, `composer test` reports **186 tests, 387 assertions, all green** under PHPUnit 12.5.6 / PHP 8.5.5 (verified via the `composer:latest` Docker image). All five originally-planned analysers from the Step D Phase B scope (B.1's three plus B.2's two) are now operational on their respective Log subclasses.
|
||||||
785
docs/superpowers/plans/2026-04-30-pz-analysers.md
Normal file
785
docs/superpowers/plans/2026-04-30-pz-analysers.md
Normal file
@@ -0,0 +1,785 @@
|
|||||||
|
# ProjectZomboid Phase B.1 ServerLog Analysers — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add three top-priority ServerLog analysers (engine version, mod load + missing-mod problem, server exception coalesced by type) by introducing five Insight classes that plug into the framework's existing `PatternAnalyser`, then wire `ProjectZomboidServerLog::getDefaultAnalyser()` to return a configured analyser carrying them.
|
||||||
|
|
||||||
|
**Architecture:** All Phase B.1 analysis is done by a single vanilla `PatternAnalyser` — no custom Analyser subclass is needed because `Entry::__toString()` joins all of an entry's lines with `\n`, and `PatternAnalyser::analyseEntry` runs `preg_match_all` against the stringified entry. A single multi-line regex on `ServerExceptionProblem` therefore captures both the ERROR header and the trailing tab-indented stack body in one match. Each Insight class declares its own `getPatterns()`/`setMatches()` and the framework coalesces equal insights via the existing `Insight::isEqual()` mechanism.
|
||||||
|
|
||||||
|
**Tech Stack:** PHP 8.4+, PHPUnit 12, Composer (root package: `indifferentketchup/codex`). PHP/Composer not installed on host — all command invocations wrap in `docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest …`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-04-30-pz-analysers-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pre-flight
|
||||||
|
|
||||||
|
The test runner across this whole plan is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
To run a single test file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Project root is the repository root. All paths in this plan are relative to it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Purpose | Created/Modified |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/Analysis/ProjectZomboid/EngineVersionInformation.php` | Information capturing the version banner (one per file) | Create |
|
||||||
|
| `src/Analysis/ProjectZomboid/ModLoadInformation.php` | Information per `loading <mod>` line, coalesced by mod name | Create |
|
||||||
|
| `src/Analysis/ProjectZomboid/ModMissingProblem.php` | Problem per missing mod, attaches a `ModMissingSolution` | Create |
|
||||||
|
| `src/Analysis/ProjectZomboid/ModMissingSolution.php` | Solution attached to `ModMissingProblem` | Create |
|
||||||
|
| `src/Analysis/ProjectZomboid/ServerExceptionProblem.php` | Problem capturing exception type + stack body, coalesced by type | Create |
|
||||||
|
| `src/Pattern/ProjectZomboid/DebugServerPattern.php` | Add new `EXCEPTION` constant for header+body capture | Modify |
|
||||||
|
| `src/Log/ProjectZomboid/ProjectZomboidServerLog.php` | Wire `getDefaultAnalyser()` to register all four insight classes | Modify |
|
||||||
|
| `test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php` | Unit test for the engine-version insight | Create |
|
||||||
|
| `test/tests/Games/ProjectZomboid/Analysis/ModLoadInformationTest.php` | Unit test for the mod-load insight | Create |
|
||||||
|
| `test/tests/Games/ProjectZomboid/Analysis/ModMissingProblemTest.php` | Unit test for the missing-mod problem and its solution | Create |
|
||||||
|
| `test/tests/Games/ProjectZomboid/Analysis/ServerExceptionProblemTest.php` | Unit test for exception type+body capture and coalescing | Create |
|
||||||
|
| `test/tests/Games/ProjectZomboid/Analyser/ServerLogAnalysisTest.php` | End-to-end test: parse fixture → analyse → assert insight set | Create |
|
||||||
|
|
||||||
|
No test fixture changes — the existing synthetic `test/src/Games/ProjectZomboid/fixtures/debug-server-minimal.txt` already contains everything the end-to-end test needs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 0: Pre-phase-B checkpoint
|
||||||
|
|
||||||
|
A revert anchor before adding new code.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the empty checkpoint commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit --allow-empty -m "pre-phase-B checkpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: EngineVersionInformation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Analysis/ProjectZomboid/EngineVersionInformation.php`
|
||||||
|
- Test: `test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php`
|
||||||
|
|
||||||
|
The pattern source `DebugServerPattern::VERSION` already exists (Phase A); this task only consumes it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analysis;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\EngineVersionInformation;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class EngineVersionInformationTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetPatternsReturnsTheVersionRegex(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([DebugServerPattern::VERSION], EngineVersionInformation::getPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMatchesPopulatesLabelAndValue(): void
|
||||||
|
{
|
||||||
|
$line = '[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.';
|
||||||
|
$this->assertSame(1, preg_match(DebugServerPattern::VERSION, $line, $matches));
|
||||||
|
|
||||||
|
$insight = new EngineVersionInformation();
|
||||||
|
$insight->setMatches($matches, 0);
|
||||||
|
|
||||||
|
$this->assertSame('Engine version', $insight->getLabel());
|
||||||
|
$this->assertSame('42.16.3 (build 0000000000000000000000000000000000000000, 2026-04-08 11:54:01)', $insight->getValue());
|
||||||
|
$this->assertSame('Engine version: 42.16.3 (build 0000000000000000000000000000000000000000, 2026-04-08 11:54:01)', $insight->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL with "Class \"IndifferentKetchup\\Codex\\Analysis\\ProjectZomboid\\EngineVersionInformation\" not found".
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
|
Create `src/Analysis/ProjectZomboid/EngineVersionInformation.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class EngineVersionInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::VERSION];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Engine version');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s (build %s, %s %s)',
|
||||||
|
$matches['version'],
|
||||||
|
$matches['hash'],
|
||||||
|
$matches['date'],
|
||||||
|
$matches['time']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests PASS, count increased by 2.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Analysis/ProjectZomboid/EngineVersionInformation.php test/tests/Games/ProjectZomboid/Analysis/EngineVersionInformationTest.php
|
||||||
|
git commit -m "Add EngineVersionInformation insight"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: ModLoadInformation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Analysis/ProjectZomboid/ModLoadInformation.php`
|
||||||
|
- Test: `test/tests/Games/ProjectZomboid/Analysis/ModLoadInformationTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `test/tests/Games/ProjectZomboid/Analysis/ModLoadInformationTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analysis;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModLoadInformation;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ModLoadInformationTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetPatternsReturnsTheModLoadRegex(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([DebugServerPattern::MOD_LOAD], ModLoadInformation::getPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMatchesExtractsModName(): void
|
||||||
|
{
|
||||||
|
$line = '[16-04-26 00:01:19.131] LOG : Mod f:0, t:1776297679131, st:48,648,194,309> loading example_mod_alpha.';
|
||||||
|
$this->assertSame(1, preg_match(DebugServerPattern::MOD_LOAD, $line, $matches));
|
||||||
|
|
||||||
|
$insight = new ModLoadInformation();
|
||||||
|
$insight->setMatches($matches, 0);
|
||||||
|
|
||||||
|
$this->assertSame('Mod loaded', $insight->getLabel());
|
||||||
|
$this->assertSame('example_mod_alpha', $insight->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsEqualCoalescesSameMod(): void
|
||||||
|
{
|
||||||
|
$a = $this->insightFor('example_mod_alpha');
|
||||||
|
$b = $this->insightFor('example_mod_alpha');
|
||||||
|
$c = $this->insightFor('example_mod_beta');
|
||||||
|
|
||||||
|
$this->assertTrue($a->isEqual($b));
|
||||||
|
$this->assertFalse($a->isEqual($c));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function insightFor(string $modName): ModLoadInformation
|
||||||
|
{
|
||||||
|
$insight = new ModLoadInformation();
|
||||||
|
$insight->setMatches(['mod' => $modName], 0);
|
||||||
|
return $insight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analysis/ModLoadInformationTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL with class-not-found.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write the implementation**
|
||||||
|
|
||||||
|
Create `src/Analysis/ProjectZomboid/ModLoadInformation.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ModLoadInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::MOD_LOAD];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Mod loaded');
|
||||||
|
$this->setValue($matches['mod']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `Information::isEqual` (label + value match) covers the coalescing requirement — no override needed.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests PASS, count increased by 3.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Analysis/ProjectZomboid/ModLoadInformation.php test/tests/Games/ProjectZomboid/Analysis/ModLoadInformationTest.php
|
||||||
|
git commit -m "Add ModLoadInformation insight"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: ModMissingProblem and ModMissingSolution
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Analysis/ProjectZomboid/ModMissingSolution.php`
|
||||||
|
- Create: `src/Analysis/ProjectZomboid/ModMissingProblem.php`
|
||||||
|
- Test: `test/tests/Games/ProjectZomboid/Analysis/ModMissingProblemTest.php`
|
||||||
|
|
||||||
|
These two ship together because `ModMissingSolution` is meaningful only as a child of `ModMissingProblem`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `test/tests/Games/ProjectZomboid/Analysis/ModMissingProblemTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analysis;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModMissingProblem;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModMissingSolution;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ModMissingProblemTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetPatternsReturnsTheModMissingRegex(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([DebugServerPattern::MOD_MISSING], ModMissingProblem::getPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMatchesExtractsModNameAndAttachesSolution(): void
|
||||||
|
{
|
||||||
|
$line = '[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.';
|
||||||
|
$this->assertSame(1, preg_match(DebugServerPattern::MOD_MISSING, $line, $matches));
|
||||||
|
|
||||||
|
$problem = new ModMissingProblem();
|
||||||
|
$problem->setMatches($matches, 0);
|
||||||
|
|
||||||
|
$this->assertSame('absent_mod', $problem->getModName());
|
||||||
|
$this->assertStringContainsString('absent_mod', $problem->getMessage());
|
||||||
|
$this->assertCount(1, $problem->getSolutions());
|
||||||
|
|
||||||
|
$solution = $problem->getSolutions()[0];
|
||||||
|
$this->assertInstanceOf(ModMissingSolution::class, $solution);
|
||||||
|
$this->assertStringContainsString('absent_mod', $solution->getMessage());
|
||||||
|
$this->assertStringContainsString('serverconfig.ini', $solution->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsEqualCoalescesSameMissingMod(): void
|
||||||
|
{
|
||||||
|
$a = $this->problemFor('mod_x');
|
||||||
|
$b = $this->problemFor('mod_x');
|
||||||
|
$c = $this->problemFor('mod_y');
|
||||||
|
|
||||||
|
$this->assertTrue($a->isEqual($b));
|
||||||
|
$this->assertFalse($a->isEqual($c));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function problemFor(string $modName): ModMissingProblem
|
||||||
|
{
|
||||||
|
$problem = new ModMissingProblem();
|
||||||
|
$problem->setMatches(['mod' => $modName], 0);
|
||||||
|
return $problem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analysis/ModMissingProblemTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL with class-not-found.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write `ModMissingSolution`**
|
||||||
|
|
||||||
|
Create `src/Analysis/ProjectZomboid/ModMissingSolution.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Solution;
|
||||||
|
|
||||||
|
class ModMissingSolution extends Solution
|
||||||
|
{
|
||||||
|
private string $modName = '';
|
||||||
|
|
||||||
|
public function setModName(string $modName): static
|
||||||
|
{
|
||||||
|
$this->modName = $modName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Subscribe to mod "%s" or remove its ID from the Mods= line in serverconfig.ini.',
|
||||||
|
$this->modName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write `ModMissingProblem`**
|
||||||
|
|
||||||
|
Create `src/Analysis/ProjectZomboid/ModMissingProblem.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ModMissingProblem extends Problem implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
private string $modName = '';
|
||||||
|
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::MOD_MISSING];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->modName = $matches['mod'];
|
||||||
|
$this->addSolution((new ModMissingSolution())->setModName($this->modName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModName(): string
|
||||||
|
{
|
||||||
|
return $this->modName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf('Required mod "%s" not found.', $this->modName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self && $insight->getModName() === $this->modName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run all tests to verify pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests PASS, count increased by 3.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Analysis/ProjectZomboid/ModMissingProblem.php src/Analysis/ProjectZomboid/ModMissingSolution.php test/tests/Games/ProjectZomboid/Analysis/ModMissingProblemTest.php
|
||||||
|
git commit -m "Add ModMissingProblem and ModMissingSolution"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: ServerExceptionProblem (with new EXCEPTION pattern constant)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Pattern/ProjectZomboid/DebugServerPattern.php` (add `EXCEPTION` constant)
|
||||||
|
- Create: `src/Analysis/ProjectZomboid/ServerExceptionProblem.php`
|
||||||
|
- Test: `test/tests/Games/ProjectZomboid/Analysis/ServerExceptionProblemTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `test/tests/Games/ProjectZomboid/Analysis/ServerExceptionProblemTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analysis;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ServerExceptionProblem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ServerExceptionProblemTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetPatternsReturnsTheExceptionRegex(): void
|
||||||
|
{
|
||||||
|
$this->assertSame([DebugServerPattern::EXCEPTION], ServerExceptionProblem::getPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMatchesCapturesTypeAndBodyAcrossLines(): void
|
||||||
|
{
|
||||||
|
$entryText = "[16-04-26 00:01:19.080] ERROR: General f:0, t:1776297679080, st:48,648,194,258> DebugFileWatcher.registerDir> Exception thrown\n"
|
||||||
|
. "\tjava.nio.file.NoSuchFileException: /placeholder/config/mods at UnixException.translateToIOException(null:-1).\n"
|
||||||
|
. "\tStack trace:\n"
|
||||||
|
. "\t\tjava.base/sun.nio.fs.UnixException.translateToIOException(Unknown Source)";
|
||||||
|
|
||||||
|
$this->assertSame(1, preg_match(DebugServerPattern::EXCEPTION, $entryText, $matches));
|
||||||
|
|
||||||
|
$problem = new ServerExceptionProblem();
|
||||||
|
$problem->setMatches($matches, 0);
|
||||||
|
|
||||||
|
$this->assertSame('java.nio.file.NoSuchFileException', $problem->getExceptionType());
|
||||||
|
$this->assertStringContainsString('Stack trace', $problem->getBody());
|
||||||
|
$this->assertStringContainsString('java.base/sun.nio.fs.UnixException', $problem->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsEqualCoalescesSameTypeRegardlessOfBody(): void
|
||||||
|
{
|
||||||
|
$a = $this->problemFor('java.io.IOException', 'body one');
|
||||||
|
$b = $this->problemFor('java.io.IOException', 'body two completely different');
|
||||||
|
$c = $this->problemFor('java.lang.RuntimeException', 'body one');
|
||||||
|
|
||||||
|
$this->assertTrue($a->isEqual($b));
|
||||||
|
$this->assertFalse($a->isEqual($c));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNestedExceptionTypeNamesAreSupported(): void
|
||||||
|
{
|
||||||
|
$entryText = "[16-04-26 00:01:45.937] ERROR: WorldGen f:0, t:1776297705937, st:48,648,221,115> IsoPropertyType.lookupOrDefaultStr> Exception thrown\n"
|
||||||
|
. "\tzombie.core.properties.IsoPropertyType\$IsoPropertyTypeNotFoundException: Property Name not found: ladderW";
|
||||||
|
|
||||||
|
$this->assertSame(1, preg_match(DebugServerPattern::EXCEPTION, $entryText, $matches));
|
||||||
|
|
||||||
|
$problem = new ServerExceptionProblem();
|
||||||
|
$problem->setMatches($matches, 0);
|
||||||
|
|
||||||
|
$this->assertSame('zombie.core.properties.IsoPropertyType$IsoPropertyTypeNotFoundException', $problem->getExceptionType());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function problemFor(string $type, string $body): ServerExceptionProblem
|
||||||
|
{
|
||||||
|
$problem = new ServerExceptionProblem();
|
||||||
|
$problem->setMatches(['type' => $type, 'body' => $body], 0);
|
||||||
|
return $problem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analysis/ServerExceptionProblemTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL with class-not-found AND constant-not-defined errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the `EXCEPTION` constant to `DebugServerPattern`**
|
||||||
|
|
||||||
|
Modify `src/Pattern/ProjectZomboid/DebugServerPattern.php`. After the existing `EXCEPTION_HEADER` constant, add:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public const string EXCEPTION = '/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\][^\n]+Exception thrown\n\t(?<type>[A-Za-z0-9_.$]+(?:Exception|Error))[^\n]*(?<body>(?:\n\t.+)*)/';
|
||||||
|
```
|
||||||
|
|
||||||
|
The full file becomes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?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/';
|
||||||
|
|
||||||
|
public const string EXCEPTION = '/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\][^\n]+Exception thrown\n\t(?<type>[A-Za-z0-9_.$]+(?:Exception|Error))[^\n]*(?<body>(?:\n\t.+)*)/';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write the implementation**
|
||||||
|
|
||||||
|
Create `src/Analysis/ProjectZomboid/ServerExceptionProblem.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ServerExceptionProblem extends Problem implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
private string $exceptionType = '';
|
||||||
|
private string $body = '';
|
||||||
|
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::EXCEPTION];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->exceptionType = $matches['type'];
|
||||||
|
$this->body = trim($matches['body'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExceptionType(): string
|
||||||
|
{
|
||||||
|
return $this->exceptionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBody(): string
|
||||||
|
{
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf('Exception thrown: %s', $this->exceptionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self
|
||||||
|
&& $insight->getExceptionType() === $this->exceptionType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run all tests to verify pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests PASS, count increased by 4.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Pattern/ProjectZomboid/DebugServerPattern.php src/Analysis/ProjectZomboid/ServerExceptionProblem.php test/tests/Games/ProjectZomboid/Analysis/ServerExceptionProblemTest.php
|
||||||
|
git commit -m "Add ServerExceptionProblem insight"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Wire ProjectZomboidServerLog default analyser + end-to-end test
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Log/ProjectZomboid/ProjectZomboidServerLog.php`
|
||||||
|
- Test: `test/tests/Games/ProjectZomboid/Analyser/ServerLogAnalysisTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing end-to-end test**
|
||||||
|
|
||||||
|
Create `test/tests/Games/ProjectZomboid/Analyser/ServerLogAnalysisTest.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\EngineVersionInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModLoadInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModMissingProblem;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ServerExceptionProblem;
|
||||||
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidServerLog;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ServerLogAnalysisTest extends TestCase
|
||||||
|
{
|
||||||
|
private function fixturePath(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/debug-server-minimal.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnalyseProducesExpectedInsightSet(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$this->assertCount(1, $analysis->getFilteredInsights(EngineVersionInformation::class));
|
||||||
|
$this->assertCount(3, $analysis->getFilteredInsights(ModLoadInformation::class));
|
||||||
|
$this->assertCount(1, $analysis->getFilteredInsights(ModMissingProblem::class));
|
||||||
|
$this->assertCount(2, $analysis->getFilteredInsights(ServerExceptionProblem::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAnalysisCarriesAttachedSolutionForMissingMod(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$missing = $analysis->getFilteredInsights(ModMissingProblem::class);
|
||||||
|
$this->assertCount(1, $missing);
|
||||||
|
$this->assertCount(1, $missing[0]->getSolutions());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testTwoDistinctExceptionsAreNotCoalesced(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidServerLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$exceptions = $analysis->getFilteredInsights(ServerExceptionProblem::class);
|
||||||
|
$types = array_map(fn($e) => $e->getExceptionType(), $exceptions);
|
||||||
|
sort($types);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
[
|
||||||
|
'java.nio.file.NoSuchFileException',
|
||||||
|
'zombie.core.properties.IsoPropertyType$IsoPropertyTypeNotFoundException',
|
||||||
|
],
|
||||||
|
$types
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest vendor/bin/phpunit test/tests/Games/ProjectZomboid/Analyser/ServerLogAnalysisTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL — `ProjectZomboidServerLog::getDefaultAnalyser()` currently returns an empty `PatternAnalyser` with no insight classes registered, so all four `getFilteredInsights` calls return zero items and the count assertions fail.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Wire `ProjectZomboidServerLog::getDefaultAnalyser()`**
|
||||||
|
|
||||||
|
Modify `src/Log/ProjectZomboid/ProjectZomboidServerLog.php`. Replace the body of `getDefaultAnalyser()`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(EngineVersionInformation::class)
|
||||||
|
->addPossibleInsightClass(ModLoadInformation::class)
|
||||||
|
->addPossibleInsightClass(ModMissingProblem::class)
|
||||||
|
->addPossibleInsightClass(ServerExceptionProblem::class);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the four corresponding `use` statements at the top of the file (after the existing `use` lines):
|
||||||
|
|
||||||
|
```php
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\EngineVersionInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModLoadInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModMissingProblem;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ServerExceptionProblem;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run all tests to verify pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v "$(pwd):/app" -w /app -u "$(id -u):$(id -g)" composer:latest composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests PASS, count increased by 3.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Log/ProjectZomboid/ProjectZomboidServerLog.php test/tests/Games/ProjectZomboid/Analyser/ServerLogAnalysisTest.php
|
||||||
|
git commit -m "Wire ProjectZomboidServerLog default analyser"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Done condition
|
||||||
|
|
||||||
|
After Task 5, `composer test` should report 158 tests, 309 assertions, all green:
|
||||||
|
|
||||||
|
- 146 baseline (from end of Phase A)
|
||||||
|
- +2 (Task 1)
|
||||||
|
- +3 (Task 2)
|
||||||
|
- +3 (Task 3)
|
||||||
|
- +4 (Task 4)
|
||||||
|
- +3 (Task 5 e2e)
|
||||||
|
|
||||||
|
If counts diverge from this projection, stop and investigate before claiming completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase B.2 deferred
|
||||||
|
|
||||||
|
PvpDamageAnalyser and AdminAuditAnalyser ride into a separate spec + plan in a follow-up session. The empty `src/Analyser/ProjectZomboid/.gitkeep` placeholder stays untouched until those analysers land.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# ProjectZomboid analyser design (Phase B.3 — deferred analysers)
|
||||||
|
|
||||||
|
> Retroactive: written 2026-05-01.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add the three remaining Project Zomboid analysers from the original Step D candidate list — connection failure pairing, item duplication heuristic, and skill progression anomaly detection — by introducing custom `Analyser` subclasses under `src/Analyser/ProjectZomboid/`. These are the first analysers in the tree that cannot be expressed as configured `PatternAnalyser` instances; they require cross-entry state (event pairing, sliding windows, snapshot deltas) that `PatternAnalyser` does not provide.
|
||||||
|
|
||||||
|
This document covers Phase B.3. Phase B.1 / B.2 docs are at `2026-04-30-pz-analysers-design.md` / `2026-04-30-pz-analysers-pvp-admin-design.md`. With Phase B.3, the original eight-analyser candidate list from Step D is fully implemented.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope:** `ConnectionFailureAnalyser` + `ConnectionFailureProblem` (UserLog, event pairing); `ItemDuplicationAnalyser` + `ItemDuplicationProblem` (ItemLog, sliding-window heuristic); `SkillProgressionAnomalyAnalyser` + `SkillProgressionAnomalyProblem` (PerkLog, consecutive-snapshot delta); wiring three Log subclasses' `getDefaultAnalyser()`; extending two synthetic fixtures to exercise trigger and non-trigger cases; end-to-end tests.
|
||||||
|
- **Out of scope (B.3):** the five other PZ logs whose `getDefaultAnalyser()` continues returning an empty `PatternAnalyser` stub (Chat, ClientAction, Cmd, Map, BurdJournals); the codex-side `Redactor` utility; Hytale / Minecraft / Seven Days To Die analysers; v0.1.0 release plumbing.
|
||||||
|
|
||||||
|
## Architectural shift: custom `Analyser` subclasses
|
||||||
|
|
||||||
|
Phases B.1 and B.2 established the convention that vanilla `PatternAnalyser` plus `Insight::isEqual()` coalescing is sufficient for per-entry pattern matching, and a custom Analyser subclass is **not** needed even for multi-line records (PatternParser's continuation-line behaviour combined with `Entry::__toString()` joins solves multi-line capture without subclassing).
|
||||||
|
|
||||||
|
Phase B.3's three analysers genuinely require cross-entry state:
|
||||||
|
|
||||||
|
- **ConnectionFailureAnalyser** must count `attempting to join` and `allowed to join` events per Steam ID and report unmatched attempts. PatternAnalyser dispatches each entry independently and has no mechanism to compare counts across entries.
|
||||||
|
- **ItemDuplicationAnalyser** must group positive-delta item events by `(steamid, item)` tuple and slide a fixed-second window across each group. Sliding-window logic spans multiple entries by definition.
|
||||||
|
- **SkillProgressionAnomalyAnalyser** must collect all perks-row snapshots per Steam ID, sort them by time, then compute pairwise deltas between consecutive snapshots. Pairwise comparison spans entries.
|
||||||
|
|
||||||
|
Each subclass extends the framework's abstract `Analyser`, overrides `analyse(): AnalysisInterface`, walks `$this->log` once to aggregate state, and emits `Problem` insights at the end. The CLAUDE.md "Framework architecture" section was updated alongside Phase B.3 to document this pattern.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
Three `Analyser` subclasses under `src/Analyser/ProjectZomboid/` (the directory's `.gitkeep` placeholder is removed in this phase):
|
||||||
|
|
||||||
|
| Analyser | Target Log | Logic shape | Threshold constants |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ConnectionFailureAnalyser` | `ProjectZomboidUserLog` | Two-pass count of attempt vs allowed events per Steam ID; emits one Problem per Steam ID where attempts > allowed | None — strict pairing |
|
||||||
|
| `ItemDuplicationAnalyser` | `ProjectZomboidItemLog` | Sliding-window heuristic over `(steamid, item)` groups | `THRESHOLD_COUNT = 5`, `THRESHOLD_WINDOW_SECONDS = 10` |
|
||||||
|
| `SkillProgressionAnomalyAnalyser` | `ProjectZomboidPerkLog` | Consecutive-snapshot delta per `(steamid, skill)`; only positive-delta perks-row entries (Login/Logout/LevelUp event tokens are filtered out) | `THRESHOLD_DELTA = 3` |
|
||||||
|
|
||||||
|
Three `Problem` subclasses under `src/Analysis/ProjectZomboid/`:
|
||||||
|
|
||||||
|
| Problem | Coalescing |
|
||||||
|
|---|---|
|
||||||
|
| `ConnectionFailureProblem` | By Steam ID — one problem per player regardless of how many unmatched attempts |
|
||||||
|
| `ItemDuplicationProblem` | By `(steamid, item)` tuple — one problem per suspicious group |
|
||||||
|
| `SkillProgressionAnomalyProblem` | By `(steamid, skill)` — one problem per skill exceeding the delta threshold |
|
||||||
|
|
||||||
|
## Threshold rationale (recorded as docblocks)
|
||||||
|
|
||||||
|
The constants are first-pass heuristics expected to be tuned once production logs flow through codex. Each is documented inline in its analyser class:
|
||||||
|
|
||||||
|
- **`ItemDuplicationAnalyser::THRESHOLD_COUNT = 5`**: Five identical item gains in a fixed window. Legitimate gameplay rarely produces five identical items quickly — crafting has animation delays, looting is one-at-a-time, zombie drops are similarly serial. A burst of five suggests admin-spawn or exploit. Tune downward if false negatives appear.
|
||||||
|
- **`ItemDuplicationAnalyser::THRESHOLD_WINDOW_SECONDS = 10`**: Ten seconds covers a realistic burst-loot scenario (e.g. a crate full of identical items) without collapsing onto unrelated events. Combined with `THRESHOLD_COUNT` this means an effective rate of 0.5 same-item events per second.
|
||||||
|
- **`SkillProgressionAnomalyAnalyser::THRESHOLD_DELTA = 3`**: PZ skills require thousands of XP per level; even active grinding rarely produces four-or-more level jumps in a single session bridge. Set to 3 as baseline; modded XP servers may need to raise this via subclass override.
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
No new pattern constants. Existing constants from Phase A are reused inside the per-entry walks:
|
||||||
|
|
||||||
|
- `UserPattern::PLAYER_EVENT` — decode `[time] <steamid> "<player>" <event>` lines
|
||||||
|
- `ItemPattern::FIELDS` — decode `[time] <steamid> "<player>" <location> <delta> <coords> [<item>]` lines
|
||||||
|
- `PerkPattern::FIELDS` — decode the bracket-heavy perks log line
|
||||||
|
- `PerkPattern::PERK_PAIR` — extract individual `Skill=N` pairs from the perks-row event field
|
||||||
|
|
||||||
|
`Entry::getTime()` returns integer Unix seconds (sub-second precision is dropped by `DateTime::getTimestamp()`). For `ItemDuplicationAnalyser` this means events within the same second collapse to time-diff zero, which is acceptable for v1.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
Three `getDefaultAnalyser()` overrides (each was previously `return new PatternAnalyser();`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ProjectZomboidUserLog
|
||||||
|
return new ConnectionFailureAnalyser();
|
||||||
|
|
||||||
|
// ProjectZomboidItemLog
|
||||||
|
return new ItemDuplicationAnalyser();
|
||||||
|
|
||||||
|
// ProjectZomboidPerkLog
|
||||||
|
return new SkillProgressionAnomalyAnalyser();
|
||||||
|
```
|
||||||
|
|
||||||
|
The unused `PatternAnalyser` import is removed from each Log subclass.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
End-to-end tests under `test/tests/Games/ProjectZomboid/Analyser/`, one per Log:
|
||||||
|
|
||||||
|
- **`UserLogAnalysisTest`** — drives `user-minimal.txt`. Asserts exactly one `ConnectionFailureProblem` for Player1 (Steam ID `76561198000000001`) with `unmatchedAttempts == 1` (Player1 has two `attempting to join` events, one of which is `attempting to join used queue`, and one `allowed to join`). Asserts that Player2 (matched 1+1) is not flagged.
|
||||||
|
- **`ItemLogAnalysisTest`** — drives the extended `item-minimal.txt`. Asserts one `ItemDuplicationProblem` for AdminUser + Base.Bullets9mm with `eventCount == 6`, and verifies the four-event Base.Plank group does not trigger. Also asserts the threshold constants are positive and documented.
|
||||||
|
- **`PerkLogAnalysisTest`** — drives the extended `perk-minimal.txt`. Asserts exactly two `SkillProgressionAnomalyProblem` insights for PlayerSuspect (Steam ID `76561198000000004`), one for Strength (delta +8) and one for Fitness (delta +6). Verifies that Maintenance (delta exactly +3) does not trigger because the comparison is strict `>`. Verifies that single-snapshot players (Player1, Player2) are not flagged. Asserts the threshold constant is positive and documented.
|
||||||
|
|
||||||
|
## Fixture changes
|
||||||
|
|
||||||
|
Two synthetic fixtures extended (no new files, no real-log content):
|
||||||
|
|
||||||
|
- **`item-minimal.txt`** — appended 10 lines: a 6-event Bullets9mm burst by AdminUser at sub-second timestamps `19:50:00.001`–`.006` (triggers the dupe heuristic), and a 4-event Plank group by Player1 scattered across 4 minutes (`20:00:00`–`20:03:00`, sub-threshold). The Phase A entry-count assertion in `ProjectZomboidItemLogTest` was bumped from 10 → 20.
|
||||||
|
- **`perk-minimal.txt`** — appended 4 lines: PlayerSuspect (Steam ID `76561198000000004`) with two perks snapshots — a low-stat baseline at `18:30:00.000` and an inflated set at `22:00:00.000` showing Strength 2→10, Fitness 2→8, and Maintenance 0→3 (boundary case). The Phase A entry-count assertion in `ProjectZomboidPerkLogTest` was bumped from 6 → 10.
|
||||||
|
|
||||||
|
All identifiers are placeholder per the Privacy / Fixture Rules in CLAUDE.md (`76561198000000001`–`76561198000000004` for Steam IDs, `Player1`/`Player2`/`AdminUser`/`PlayerSuspect` for names, coords in the `1000-1100, 2000-2200, 0` range).
|
||||||
|
|
||||||
|
## Commits (as-built, in order)
|
||||||
|
|
||||||
|
1. `c444e85` — `pre-phase-B.3 checkpoint` (`--allow-empty`)
|
||||||
|
2. `73e9ca6` — `Add ConnectionFailureAnalyser`
|
||||||
|
3. `ba3fae8` — `Add ItemDuplicationAnalyser`
|
||||||
|
4. `0c90e40` — `Add SkillProgressionAnomalyAnalyser`
|
||||||
|
|
||||||
|
4 commits total. Each non-checkpoint commit ships an Analyser + Problem + (optional) fixture extension + updated count assertion + e2e test in one logical unit, per the per-analyser commit shape requested up front.
|
||||||
|
|
||||||
|
## Open issues
|
||||||
|
|
||||||
|
None blocking. All three threshold constants are heuristic guesses pending production data calibration; tuning is expected once iblogs starts feeding real logs through codex. The values are tunable via subclass override and the rationale is in the source docblocks.
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Phase B.1 (foundation, ServerLog analysers): `2026-04-30-pz-analysers-design.md` and `2026-04-30-pz-analysers.md`.
|
||||||
|
- Phase B.2 (vanilla PatternAnalyser PvP/Admin coverage): `2026-04-30-pz-analysers-pvp-admin-design.md` and `2026-04-30-pz-analysers-pvp-admin.md`.
|
||||||
|
- Workflow conventions and architecture overview: `CLAUDE.md`.
|
||||||
|
- The Phase B.3 commit set begins at `c444e85` (pre-checkpoint) and ends at `0c90e40` (the third analyser).
|
||||||
121
docs/superpowers/specs/2026-04-30-pz-analysers-design.md
Normal file
121
docs/superpowers/specs/2026-04-30-pz-analysers-design.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# ProjectZomboid analyser design (Phase B.1)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Implement the three top-priority ServerLog analysers — engine version, mod load order plus missing-mod problems, and server exception coalescing — by adding five Insight classes that plug into the framework's existing `PatternAnalyser`. Wire `ProjectZomboidServerLog::getDefaultAnalyser()` to return a configured `PatternAnalyser` carrying all four insight classes (`ModMissingSolution` is a Solution attached to `ModMissingProblem`, not a separately registered insight).
|
||||||
|
|
||||||
|
This document covers Phase B.1. Phase B.2 (PvpDamageAnalyser and AdminAuditAnalyser) ships separately and gets its own spec.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope:** All work needed to make `(new ProjectZomboidServerLog())->setLogFile(path)->parse()->analyse()` return an `Analysis` populated with engine-version information, mod-load information, missing-mod problems with attached solutions, and server-exception problems coalesced by exception type.
|
||||||
|
- **Out of scope (B.1):** PvP damage, admin audit, codex-side redaction, custom Solution wording for `ServerExceptionProblem`, Hytale/Minecraft/SevenDaysToDie analysers, the empty `src/Analyser/ProjectZomboid/.gitkeep` placeholder.
|
||||||
|
|
||||||
|
## Architectural decision: no Analyser subclasses
|
||||||
|
|
||||||
|
The original Step-D plan called for a custom `ServerExceptionAnalyser` subclass to capture the tab-indented stack-trace lines that follow each `ERROR` header. On a closer reading of the framework, this is unnecessary:
|
||||||
|
|
||||||
|
- `Entry::__toString()` joins all of an entry's `Line`s with `\n`.
|
||||||
|
- `PatternAnalyser::analyseEntry()` calls `preg_match_all($pattern, $entry, ...)` against the stringified entry.
|
||||||
|
- A regex with the `s` flag captures across the embedded newlines and grabs the stack body in the same match.
|
||||||
|
|
||||||
|
The single `PatternAnalyser` instance configured with multiple insight classes covers all three analysers. No subclassing required.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
All under `src/Analysis/ProjectZomboid/`:
|
||||||
|
|
||||||
|
| Class | Type | Purpose | Coalescing |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `EngineVersionInformation` | Information | Capture `version=X.Y.Z <hash> <date> <time>` | Always equal (single engine version per file) |
|
||||||
|
| `ModLoadInformation` | Information | Capture each `loading <modId>` line | Equal when `mod` field matches |
|
||||||
|
| `ModMissingProblem` | Problem | Capture each `required mod "X" not found` warning; attach a `ModMissingSolution` | Equal when missing-mod name matches |
|
||||||
|
| `ModMissingSolution` | Solution | Pragmatic guidance ("Subscribe to the missing mod or remove its ID from the `Mods=` line in `serverconfig.ini`.") | n/a |
|
||||||
|
| `ServerExceptionProblem` | Problem | Capture exception header and the trailing tab-indented stack body in one match (multi-line regex with `s` flag) | Equal when exception-type string matches; first body wins, counter increments |
|
||||||
|
|
||||||
|
`ModMissingSolution` is constructed and attached inside `ModMissingProblem::setMatches()` so callers don't have to wire it manually.
|
||||||
|
|
||||||
|
`ServerExceptionProblem` overrides `isEqual()` to compare only the exception-type token. This deviates from the default `Information::isEqual` behaviour (label + value match) because the value field includes the (variable) stack body and we want different bodies of the same exception type to coalesce.
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
All Phase B.1 patterns live on `DebugServerPattern` (Phase A class). Existing constants reused as-is:
|
||||||
|
|
||||||
|
- `VERSION` — `/version=(?<version>\S+) (?<hash>[a-f0-9]{40}) (?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2})/`
|
||||||
|
- `MOD_LOAD` — `/loading (?<mod>[A-Za-z0-9_]+)\.?$/`
|
||||||
|
- `MOD_MISSING` — `/required mod "(?<mod>[^"]+)" not found/`
|
||||||
|
|
||||||
|
One new constant added:
|
||||||
|
|
||||||
|
- `EXCEPTION` — anchored at entry start, captures both the header line and the trailing tab-indented stack body in one match. Named groups: `type` (the FQCN of the thrown exception, parsed from the first body line) and `body` (zero-or-more additional indented stack frames).
|
||||||
|
```
|
||||||
|
'/^\[\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\][^\n]+Exception thrown\n\t(?<type>[A-Za-z0-9_.$]+(?:Exception|Error))[^\n]*(?<body>(?:\n\t.+)*)/'
|
||||||
|
```
|
||||||
|
Note `[^\n]+` and `[^\n]*` rather than `.+` keep the regex well-behaved without the `s` flag — each character class explicitly excludes newlines, and `(?:\n\t.+)*` walks the body line-by-line. `$` inside the type class allows nested-class names like `IsoPropertyType$IsoPropertyTypeNotFoundException`.
|
||||||
|
|
||||||
|
The existing `EXCEPTION_HEADER` constant stays for any caller that only needs the header line; `EXCEPTION` is the one `ServerExceptionProblem` registers in `getPatterns()`.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
`ProjectZomboidServerLog::getDefaultAnalyser()` changes from:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return new PatternAnalyser();
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```php
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(EngineVersionInformation::class)
|
||||||
|
->addPossibleInsightClass(ModLoadInformation::class)
|
||||||
|
->addPossibleInsightClass(ModMissingProblem::class)
|
||||||
|
->addPossibleInsightClass(ServerExceptionProblem::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
The other ten ProjectZomboid log subclasses keep their empty `new PatternAnalyser()` stubs until Phase B.2 (PvpLog, AdminLog) and beyond.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
Unit tests under `test/tests/Games/ProjectZomboid/Analysis/` (one per Insight class):
|
||||||
|
|
||||||
|
- `EngineVersionInformationTest` — `getPatterns()` returns the expected regex; `setMatches` populates label/value; `getMessage` reads as `"Engine version: 42.16.3 (build 0000…0000)"` or similar concise form.
|
||||||
|
- `ModLoadInformationTest` — `setMatches` extracts `mod`; two instances with the same mod compare equal; two with different mods compare not-equal.
|
||||||
|
- `ModMissingProblemTest` — `setMatches` extracts the missing-mod name; the problem carries exactly one `ModMissingSolution`; isEqual coalesces same name.
|
||||||
|
- `ServerExceptionProblemTest` — `setMatches` extracts both type and body; isEqual returns true for same type with different bodies; isEqual returns false for different types.
|
||||||
|
|
||||||
|
End-to-end test under `test/tests/Games/ProjectZomboid/Analyser/`:
|
||||||
|
|
||||||
|
- `ServerLogAnalysisTest::testAnalyseProducesExpectedInsights` — feeds existing `debug-server-minimal.txt` through `(new ProjectZomboidServerLog())->setLogFile(...)->parse()->analyse()`. Asserts:
|
||||||
|
- 1× `EngineVersionInformation` (one version banner in fixture)
|
||||||
|
- 3× `ModLoadInformation` (alpha/beta/gamma)
|
||||||
|
- 1× `ModMissingProblem` (absent_mod) carrying 1× `ModMissingSolution`
|
||||||
|
- 2× `ServerExceptionProblem` (NoSuchFileException + IsoPropertyTypeNotFoundException, distinct types so no coalescing in this fixture)
|
||||||
|
|
||||||
|
## Fixture changes
|
||||||
|
|
||||||
|
None. The existing synthetic `test/src/Games/ProjectZomboid/fixtures/debug-server-minimal.txt` already contains exactly the lines required for the end-to-end test above. No new identifiers or coordinates introduced.
|
||||||
|
|
||||||
|
## Commits (planned)
|
||||||
|
|
||||||
|
Following CLAUDE.md workflow conventions (one logical concept per commit, run `composer test` between):
|
||||||
|
|
||||||
|
1. `Document Phase B.1 ServerLog analyser design` — this spec file under `docs/superpowers/specs/`.
|
||||||
|
2. `pre-phase-B checkpoint` — `git commit --allow-empty`.
|
||||||
|
3. `Add EngineVersionInformation insight` — Insight class + unit test.
|
||||||
|
4. `Add ModLoadInformation insight` — Insight class + unit test.
|
||||||
|
5. `Add ModMissingProblem and ModMissingSolution` — Problem + Solution paired (Solution belongs to Problem, ships together).
|
||||||
|
6. `Add ServerExceptionProblem insight` — includes the new `DebugServerPattern::EXCEPTION` constant; Problem + unit test.
|
||||||
|
7. `Wire ProjectZomboidServerLog default analyser + end-to-end test` — modifies `ProjectZomboidServerLog::getDefaultAnalyser`, adds `ServerLogAnalysisTest`.
|
||||||
|
|
||||||
|
Total: 7 commits expected (6 if the empty checkpoint produces no diff and we skip per the workflow rule — but it always produces a diff because it's `--allow-empty`).
|
||||||
|
|
||||||
|
## Open issues
|
||||||
|
|
||||||
|
None. All Phase B.1 ambiguity was resolved in the question table preceding this spec.
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Phase A (foundation): commits `8ae7da5` through `cca5208` — the 11 Log subclasses, 11 Pattern classes, and `ProjectZomboidDetective` wiring this builds on.
|
||||||
|
- Workflow conventions: `CLAUDE.md` § Workflow conventions and § Pitfalls.
|
||||||
|
- Privacy boundary: `CLAUDE.md` § Privacy / fixture rules.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# ProjectZomboid analyser design (Phase B.2)
|
||||||
|
|
||||||
|
> Retroactive: written 2026-05-01.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add Project Zomboid PvP combat detection (filtering zombie hits and zero-damage events) and admin verb-dispatch coverage of six action types, by registering seven new `Information` insight classes onto the existing `PatternAnalyser`. No custom `Analyser` subclasses are introduced in this phase — all dispatch fits within `PatternAnalyser`'s per-entry pattern matching.
|
||||||
|
|
||||||
|
This document covers Phase B.2. Phase B.1 is in `2026-04-30-pz-analysers-design.md`. Phase B.3 (cross-entry / threshold analysers requiring custom `Analyser` subclasses) is in `2026-04-30-pz-analysers-deferred-design.md`.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope:** `PvpDamageInformation` + `PvpPattern::COMBAT_REAL` regex; six `Admin<Verb>Information` classes + six `AdminPattern::<VERB>_ENTRY` regex constants; wiring `ProjectZomboidPvpLog::getDefaultAnalyser()` and `ProjectZomboidAdminLog::getDefaultAnalyser()`; end-to-end tests for both logs.
|
||||||
|
- **Out of scope (B.2):** any cross-entry / threshold / pairing logic (deferred to B.3); the eight other PZ logs whose `getDefaultAnalyser()` continues returning an empty `PatternAnalyser` stub; the codex-side `Redactor` utility (deferred — see `2026-04-30-redactor-design.md`).
|
||||||
|
|
||||||
|
## Architectural decision: vanilla PatternAnalyser
|
||||||
|
|
||||||
|
Phase B.1 established that `PatternAnalyser` plus `Insight::isEqual()` coalescing covers single-entry pattern matching cleanly. Phase B.2's analysers (PvP damage rows, admin verb lines) all fit that mould — each interesting line is independent of the others, dispatch is per-entry, and counter-coalescing handles repeats. No `Analyser` subclassing required. (Phase B.3 will deviate from this when cross-entry logic enters the picture.)
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
All under `src/Analysis/ProjectZomboid/`:
|
||||||
|
|
||||||
|
| Class | Type | Pattern | Coalescing |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `PvpDamageInformation` | Information | `PvpPattern::COMBAT_REAL` | Default `Information::isEqual` (label + value) — same attacker/victim/weapon coalesces |
|
||||||
|
| `AdminAddedItemInformation` | Information | `AdminPattern::ADDED_ITEM_ENTRY` | Default — same admin/item/target coalesces |
|
||||||
|
| `AdminAddedXpInformation` | Information | `AdminPattern::ADDED_XP_ENTRY` | Default — same admin/amount/skill/target coalesces |
|
||||||
|
| `AdminGrantedAccessInformation` | Information | `AdminPattern::GRANTED_ACCESS_ENTRY` | Default — same admin/level/target coalesces |
|
||||||
|
| `AdminChangedOptionInformation` | Information | `AdminPattern::CHANGED_OPTION_ENTRY` | Default — same admin/option/value coalesces |
|
||||||
|
| `AdminReloadedOptionsInformation` | Information | `AdminPattern::RELOADED_OPTIONS_ENTRY` | Default — same admin coalesces |
|
||||||
|
| `AdminTeleportedInformation` | Information | `AdminPattern::TELEPORTED_ENTRY` | Default — same admin/target/coords coalesces |
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
Seven new constants total.
|
||||||
|
|
||||||
|
**`PvpPattern::COMBAT_REAL`** — combat regex with the noise filter baked in. The negative lookahead `(?!zombie")` rejects zombie weapon rows; the damage clause uses alternation to match only positive non-zero floats:
|
||||||
|
|
||||||
|
```
|
||||||
|
'/Combat: "(?<attacker>[^"]+)" \([^)]+\) hit "(?<victim>[^"]+)" \([^)]+\) weapon="(?<weapon>(?!zombie")[^"]+)" damage=(?<damage>0\.0*[1-9][0-9]*|[1-9][0-9]*\.[0-9]+)/'
|
||||||
|
```
|
||||||
|
|
||||||
|
The damage alternation explicitly rejects `0.000000` and any leading-minus value because both branches require either `0.<non-zero>` or `<non-zero>.<digits>`.
|
||||||
|
|
||||||
|
**`AdminPattern::<VERB>_ENTRY`** — six entry-anchored variants of the existing body-only verb constants. Necessary because `PatternAnalyser` calls `preg_match_all` against the full Entry text (including the `[time]` prefix), so the Phase A verb constants anchored at `^<admin>` would never match. The Phase A constants stay intact for direct-message use; new ones live alongside them on the same `AdminPattern` class.
|
||||||
|
|
||||||
|
## Wiring
|
||||||
|
|
||||||
|
Two `getDefaultAnalyser()` overrides (was `return new PatternAnalyser();` for both):
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ProjectZomboidPvpLog
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(PvpDamageInformation::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
```php
|
||||||
|
// ProjectZomboidAdminLog
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(AdminAddedItemInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminAddedXpInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminGrantedAccessInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminChangedOptionInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminReloadedOptionsInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminTeleportedInformation::class);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
Unit tests under `test/tests/Games/ProjectZomboid/Analysis/`, one per Insight class — exercises `getPatterns()` shape, `setMatches()` extraction, and at least one filter-rejection case for `PvpDamageInformation` (zombie weapon and zero-damage rejection).
|
||||||
|
|
||||||
|
End-to-end tests under `test/tests/Games/ProjectZomboid/Analyser/`:
|
||||||
|
|
||||||
|
- `PvpLogAnalysisTest` against `pvp-minimal.txt`: asserts exactly three `PvpDamageInformation` insights (Bare Hands, Tire Iron (Worn), Hunting Knife). Zombie and vehicle rows must be filtered out by the regex.
|
||||||
|
- `AdminLogAnalysisTest` against `admin-minimal.txt`: asserts 2 + 2 + 2 + 2 + 1 + 2 = 11 insights across the six admin classes, with the duplicate ShotgunShells row coalescing into a single insight at `counter == 2`.
|
||||||
|
|
||||||
|
## Fixture changes
|
||||||
|
|
||||||
|
None. The Phase A synthetic fixtures `pvp-minimal.txt` and `admin-minimal.txt` already cover every code path Phase B.2 exercises.
|
||||||
|
|
||||||
|
## Commits (as-built, in order)
|
||||||
|
|
||||||
|
1. `df62da1` — `pre-phase-B.2 checkpoint` (`--allow-empty`)
|
||||||
|
2. `55f769c` — `Add PvpDamageInformation insight`
|
||||||
|
3. `90c85a0` — `Add AdminAddedItemInformation insight` ⚠️ broken — see `2026-04-30-pz-analysers-pvp-admin.md` §Deviations
|
||||||
|
4. `0d85a05` — `Fix missing closing brace in AdminPattern` (forward-fix for #3)
|
||||||
|
5. `a2faa55` — `Add AdminAddedXpInformation insight`
|
||||||
|
6. `caed04d` — `Add AdminGrantedAccessInformation insight`
|
||||||
|
7. `b7b89ef` — `Add AdminChangedOptionInformation insight`
|
||||||
|
8. `64641fa` — `Add AdminReloadedOptionsInformation insight`
|
||||||
|
9. `d15fc81` — `Add AdminTeleportedInformation insight`
|
||||||
|
10. `51eb2de` — `Wire ProjectZomboidPvpLog default analyser`
|
||||||
|
11. `c57d646` — `Wire ProjectZomboidAdminLog default analyser`
|
||||||
|
|
||||||
|
11 commits total, vs 10 originally planned. The brace-fix commit accounts for the discrepancy.
|
||||||
|
|
||||||
|
## Open issues
|
||||||
|
|
||||||
|
None blocking. Phase A Q4 (admin verb scope) was settled before B.2 began. Phase B Q2 confirmed PvP fixtures contain real combat events worth analysing.
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Phase B.1 (foundation): `2026-04-30-pz-analysers-design.md` and `2026-04-30-pz-analysers.md`.
|
||||||
|
- Phase B.3 (deferred analysers requiring custom `Analyser` subclasses): `2026-04-30-pz-analysers-deferred-design.md`.
|
||||||
|
- Workflow conventions: `CLAUDE.md` § Workflow conventions and § Pitfalls.
|
||||||
150
docs/superpowers/specs/2026-04-30-redactor-design.md
Normal file
150
docs/superpowers/specs/2026-04-30-redactor-design.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Codex Redactor utility — design spec
|
||||||
|
|
||||||
|
> Retroactive: written 2026-05-01.
|
||||||
|
> **Status: deferred — not implemented.** This is a forward-looking design captured here for backfill symmetry and to inform iblogs's upload-time PII handling.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Codex grows a small utility surface for redacting personally-identifying data from log content before it is stored, displayed, or analysed in environments where preservation of PII is unwanted. The shape is a thin generic interface plus per-game implementations that know each game's log format. iblogs is the primary line of defence (upload-time filter); codex's redactor is the optional helper consumers can call when they want codex itself to scrub data.
|
||||||
|
|
||||||
|
## Why deferred
|
||||||
|
|
||||||
|
The Phase A Step E open-questions table (Q5) marked the codex-side redactor as "defer to its own session" because the iblogs upload-time filter is the actual privacy boundary — anything codex does in this layer is a convenience, not a guarantee. Phase B (the analyser arc) shipped without the redactor and remains useful: synthetic fixtures use placeholder identifiers throughout, real Logs.zip never reaches the index, and the privacy story for codex's tests does not depend on this utility. Building it remains worthwhile when iblogs starts consuming codex output and wants a one-line option for "scrub before analyse."
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- **In scope (when this spec is implemented):** a `RedactorInterface` under `src/Util/`, a `ProjectZomboidRedactor` implementation that handles the three PII categories observed in PZ logs (Steam IDs, player names, world coordinates), per-category toggles with a defaults-on stance, replacement-string conventions matching the synthetic fixture placeholders.
|
||||||
|
- **Out of scope:** non-PZ game redactors (those land alongside their respective game implementations); UI / CLI wrappers; redaction of mod-specific identifiers (e.g. BurdJournals scientific-notation Steam IDs) — handled by an extension of the PZ implementation if/when needed; storage / persistence of redaction maps.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
+-------------------------+
|
||||||
|
| RedactorInterface |
|
||||||
|
| (src/Util/) |
|
||||||
|
| redact(string): string|
|
||||||
|
+-----------+-------------+
|
||||||
|
|
|
||||||
|
+-----------------------+-----------------------+
|
||||||
|
| |
|
||||||
|
+------------v-----------------+ +--------------v-------------+
|
||||||
|
| ProjectZomboidRedactor | | (Future) MinecraftRedactor |
|
||||||
|
| (src/Util/ProjectZomboid/) | | (src/Util/Minecraft/) |
|
||||||
|
+------------------------------+ +----------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
A thin interface in the framework's `Util` namespace. One concrete implementation per supported game, mirroring the existing components-outer-with-game-suffix layout used everywhere else in the tree (Analyser, Analysis, Detective, Log, Parser, Pattern). Future games' redactors land alongside their analyser surface.
|
||||||
|
|
||||||
|
## Why per-game implementations rather than a single regex utility
|
||||||
|
|
||||||
|
PII detection in log text is **context-sensitive**, not just regex matching:
|
||||||
|
|
||||||
|
- **Steam IDs** are 17-digit decimal numbers. Almost regexable, but care is needed not to chew through unrelated long numbers (timestamps, build numbers, GUIDs that happen to be 17 digits).
|
||||||
|
- **Player names** are arbitrary strings. They cannot be detected from text alone — a redactor needs to know the lexical contexts where names appear (`<steamid> "Name"`, `ChatMessage{author='Name'}`, `Combat: "Name"`). Without that knowledge a naive `\w+`-style match would shred the entire log.
|
||||||
|
- **Coordinates** are number triples in specific shapes (`x,y,z` after `at`, `[x,y,z]` between brackets, `(x,y,z)` in PvP combat lines). Stripping every "two commas in a row" regex match would over-redact (e.g. `f:0, t:1776297642406, st:48,648,157,584` is server metadata, not coordinates).
|
||||||
|
|
||||||
|
Per-game implementations encode the lexical contexts. PZ's redactor uses the same regex shapes Phase A's Pattern classes encode for parsing, applied in a different direction (replacement instead of extraction).
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### `src/Util/RedactorInterface.php`
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace IndifferentKetchup\Codex\Util;
|
||||||
|
|
||||||
|
interface RedactorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Return a copy of $content with PII replaced by placeholder tokens
|
||||||
|
* according to the redactor's enabled toggles.
|
||||||
|
*/
|
||||||
|
public function redact(string $content): string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A single method. Stateless from the caller's perspective; toggles are configured on the concrete implementation before `redact()` is called.
|
||||||
|
|
||||||
|
### `src/Util/ProjectZomboid/ProjectZomboidRedactor.php`
|
||||||
|
|
||||||
|
Implements `RedactorInterface`. Three independent toggles (defaults all on) and three regex-driven replacement passes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
namespace IndifferentKetchup\Codex\Util\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Util\RedactorInterface;
|
||||||
|
|
||||||
|
class ProjectZomboidRedactor implements RedactorInterface
|
||||||
|
{
|
||||||
|
private bool $redactSteamIds = true;
|
||||||
|
private bool $redactPlayerNames = true;
|
||||||
|
private bool $redactCoordinates = true;
|
||||||
|
|
||||||
|
public function redactSteamIds(bool $on): static { /* ... */ }
|
||||||
|
public function redactPlayerNames(bool $on): static { /* ... */ }
|
||||||
|
public function redactCoordinates(bool $on): static { /* ... */ }
|
||||||
|
|
||||||
|
public function redact(string $content): string
|
||||||
|
{
|
||||||
|
if ($this->redactSteamIds) { /* preg_replace */ }
|
||||||
|
if ($this->redactPlayerNames) { /* preg_replace */ }
|
||||||
|
if ($this->redactCoordinates) { /* preg_replace */ }
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replacement conventions
|
||||||
|
|
||||||
|
To match the synthetic fixture placeholders already used throughout the test suite (per the Privacy / fixture rules in CLAUDE.md):
|
||||||
|
|
||||||
|
| PII category | Replacement |
|
||||||
|
|---|---|
|
||||||
|
| Steam ID (17 decimal digits in a Steam ID context) | `76561198000000000` |
|
||||||
|
| Player name (between `"..."` after a 17-digit Steam ID, between `'...'` in `ChatMessage{author='...'}`, between `"..."` after subsystem keywords like `Combat:` / `Safety:`) | `<player>` |
|
||||||
|
| World coordinates (the `x,y,z` or `(x,y,z)` triples in PZ log lines, distinguished by leading-context anchors so server metadata triples are not stripped) | `0,0,0` |
|
||||||
|
|
||||||
|
The replacements are deliberately not reversible — codex makes no attempt to maintain a map between original and redacted values. Reversibility is a different feature scope (encryption / tokenization) and is not what this utility provides.
|
||||||
|
|
||||||
|
### Lexical anchors for the regex passes
|
||||||
|
|
||||||
|
Steam ID: `(?<![\w])(?P<sid>76561198\d{9})(?![\w])` — the `76561198` prefix matches the SteamID64 universe prefix for Steam (region "Individual"); avoids matching unrelated 17-digit numbers. Boundary classes prevent matching inside a longer alphanumeric token.
|
||||||
|
|
||||||
|
Player name (PZ-specific contexts):
|
||||||
|
- After Steam ID quoted: `(?<sid>76561198000000000) "(?P<name>[^"]+)"` → preserve the redacted Steam ID, replace the quoted name. (Redaction order matters: SIDs first, names second.)
|
||||||
|
- ChatMessage author: `ChatMessage\{chat=\w+, author='(?P<name>[^']+)',` → replace the captured author.
|
||||||
|
- PvP / Safety subsystem: `(?P<sub>Combat|Safety): "(?P<name>[^"]+)"` → replace the captured name.
|
||||||
|
|
||||||
|
Coordinates:
|
||||||
|
- ItemLog / MapLog / CmdLog `at` clauses: `at (?P<coords>[\d.]+,[\d.]+,-?[\d.]+)\.` → replace with `0,0,0.`
|
||||||
|
- ClientActionLog / PerkLog bracketed coords: `\[(?P<coords>\d+,\d+,-?\d+)\]` → replace with `[0,0,0]`
|
||||||
|
- PvP combat parenthesised coords: `\((?P<coords>\d+,\d+,-?\d+)\) (?:hit|restore|store|true|false)` — the trailing context disambiguates from server metadata triples.
|
||||||
|
|
||||||
|
These regex shapes are not yet committed to the spec implementation; tuning is expected during the actual implementation pass against the real `Logs.zip` content under `.scratch/pz/Logs/`.
|
||||||
|
|
||||||
|
## Where this fits relative to iblogs
|
||||||
|
|
||||||
|
The Phase A Step D Section e split holds: **iblogs is the primary line of defence**. iblogs filters PII at upload time, before storage, mirroring the mclogs IP/token redaction approach. Stored logs in iblogs are pre-sanitised. The codex `Redactor` is the *option* iblogs (or any other consumer) reaches for if they want codex itself to do the scrubbing — for example in a preview pipeline that wants to render redacted output without writing the raw paste to disk first, or in a dev environment where the same code path runs without iblogs's upload filter.
|
||||||
|
|
||||||
|
This means the codex Redactor is **non-load-bearing** for the privacy story. iblogs implementing redaction independently is the actual safety guarantee; codex's helper is a convenience.
|
||||||
|
|
||||||
|
## Test plan (when implemented)
|
||||||
|
|
||||||
|
Synthetic-only fixtures, no real-log content:
|
||||||
|
|
||||||
|
1. Three pairs of fixture-input / expected-output strings exercising each category in isolation.
|
||||||
|
2. One combined-input fixture demonstrating that all three categories applied to the same content produce a fully-scrubbed output.
|
||||||
|
3. Toggle tests: each of the three booleans turned off in isolation produces partial scrubbing; all three off produces an unchanged copy of input (the redactor returns input verbatim).
|
||||||
|
4. Idempotence test: `redact(redact($x)) == redact($x)`.
|
||||||
|
5. A small "negative" test: server metadata triples (`f:0, t:1776297642406, st:48,648,157,584`) are not mistaken for coordinates.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. **Should the redactor optionally preserve some structure for analysers downstream?** For example, after redaction the analysers can no longer correlate by Steam ID across events because every Steam ID is the same placeholder. Two paths: (a) accept the loss — redaction is done before storage and you don't analyse redacted content, or (b) provide a "tokenizing redactor" that maps each unique input value to a unique placeholder (`76561198000000001`, `76561198000000002`, ...) preserving cardinality. Recommend (a) for v1; (b) is its own design pass.
|
||||||
|
2. **What about `BurdJournals.txt`'s scientific-notation Steam IDs?** Phase A Step C noted these as `7.656119799341651E16` form. The PZ redactor's Steam ID regex doesn't match this shape. v1 leaves them intact (tag `[BurdJournals]` already disambiguates them as mod-internal). v2 could add a separate regex for the sci-notation form.
|
||||||
|
3. **Should `coords` redaction try to preserve relative location** (e.g. round to the nearest 1000-tile chunk so the *region* is visible without giving precise base coords)? Out of scope for v1.
|
||||||
|
|
||||||
|
## Pointers
|
||||||
|
|
||||||
|
- Phase A original Q5 deferral: `2026-04-30-pz-analysers-design.md` referenced this; the explicit deferral lived in chat (Phase A Step E open-questions table).
|
||||||
|
- iblogs upload-time filtering decisions: see the iblogs bootstrap spec at `2026-05-01-iblogs-bootstrap-design.md`.
|
||||||
|
- Existing Pattern classes that the regex shapes will mirror in reverse: `src/Pattern/ProjectZomboid/{CmdPattern,ItemPattern,MapPattern,PerkPattern,ClientActionPattern,ChatPattern,PvpPattern,UserPattern}.php`.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analyser;
|
namespace IndifferentKetchup\Codex\Analyser;
|
||||||
|
|
||||||
use Aternos\Codex\Log\AnalysableLogInterface;
|
use IndifferentKetchup\Codex\Log\AnalysableLogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Analyser
|
* Class Analyser
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analyser
|
* @package IndifferentKetchup\Codex\Analyser
|
||||||
*/
|
*/
|
||||||
abstract class Analyser implements AnalyserInterface
|
abstract class Analyser implements AnalyserInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analyser;
|
namespace IndifferentKetchup\Codex\Analyser;
|
||||||
|
|
||||||
use Aternos\Codex\Analysis\AnalysisInterface;
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
use Aternos\Codex\Log\AnalysableLogInterface;
|
use IndifferentKetchup\Codex\Log\AnalysableLogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface AnalyserInterface
|
* Interface AnalyserInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analyser
|
* @package IndifferentKetchup\Codex\Analyser
|
||||||
*/
|
*/
|
||||||
interface AnalyserInterface
|
interface AnalyserInterface
|
||||||
{
|
{
|
||||||
|
|||||||
0
src/Analyser/Hytale/.gitkeep
Normal file
0
src/Analyser/Hytale/.gitkeep
Normal file
0
src/Analyser/Minecraft/.gitkeep
Normal file
0
src/Analyser/Minecraft/.gitkeep
Normal file
@@ -1,17 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analyser;
|
namespace IndifferentKetchup\Codex\Analyser;
|
||||||
|
|
||||||
use Aternos\Codex\Analysis\Analysis;
|
use IndifferentKetchup\Codex\Analysis\Analysis;
|
||||||
use Aternos\Codex\Analysis\AnalysisInterface;
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
use Aternos\Codex\Analysis\PatternInsightInterface;
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
use Aternos\Codex\Log\EntryInterface;
|
use IndifferentKetchup\Codex\Log\EntryInterface;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PatternAnalyser
|
* Class PatternAnalyser
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analyser
|
* @package IndifferentKetchup\Codex\Analyser
|
||||||
*/
|
*/
|
||||||
class PatternAnalyser extends Analyser
|
class PatternAnalyser extends Analyser
|
||||||
{
|
{
|
||||||
|
|||||||
64
src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php
Normal file
64
src/Analyser/ProjectZomboid/ConnectionFailureAnalyser.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analyser\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\Analyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Analysis;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ConnectionFailureProblem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\UserPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pairs "attempting to join" with subsequent "allowed to join" events per
|
||||||
|
* Steam ID and flags any unmatched attempts. PatternAnalyser cannot express
|
||||||
|
* this because it operates per-entry without cross-entry state, so this
|
||||||
|
* walks the entire log once and aggregates before emitting Problems.
|
||||||
|
*
|
||||||
|
* "attempting to join used queue" is treated as an attempt; a player still
|
||||||
|
* waiting in queue at end-of-log will therefore be flagged. This is
|
||||||
|
* intentional v1 behaviour — a long-lived queue wait looks indistinguishable
|
||||||
|
* from a real failure without timing context, and surfacing both lets a
|
||||||
|
* human triage.
|
||||||
|
*/
|
||||||
|
class ConnectionFailureAnalyser extends Analyser
|
||||||
|
{
|
||||||
|
public function analyse(): AnalysisInterface
|
||||||
|
{
|
||||||
|
$analysis = new Analysis();
|
||||||
|
$analysis->setLog($this->log);
|
||||||
|
|
||||||
|
$attempts = [];
|
||||||
|
$allowed = [];
|
||||||
|
$playerName = [];
|
||||||
|
|
||||||
|
foreach ($this->log as $entry) {
|
||||||
|
$text = (string) $entry;
|
||||||
|
if (preg_match(UserPattern::PLAYER_EVENT, $text, $m) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$steamId = $m['steamid'];
|
||||||
|
$playerName[$steamId] = $m['player'];
|
||||||
|
|
||||||
|
if (str_starts_with($m['event'], 'attempting to join')) {
|
||||||
|
$attempts[$steamId] = ($attempts[$steamId] ?? 0) + 1;
|
||||||
|
} elseif (str_starts_with($m['event'], 'allowed to join')) {
|
||||||
|
$allowed[$steamId] = ($allowed[$steamId] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($attempts as $steamId => $attemptCount) {
|
||||||
|
$allowedCount = $allowed[$steamId] ?? 0;
|
||||||
|
$unmatched = $attemptCount - $allowedCount;
|
||||||
|
if ($unmatched <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$analysis->addInsight((new ConnectionFailureProblem())
|
||||||
|
->setSteamId($steamId)
|
||||||
|
->setPlayer($playerName[$steamId] ?? '')
|
||||||
|
->setUnmatchedAttempts($unmatched));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $analysis;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php
Normal file
90
src/Analyser/ProjectZomboid/ItemDuplicationAnalyser.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analyser\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\Analyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Analysis;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ItemDuplicationProblem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\ItemPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flags suspicious item-gain frequency per (player, item) tuple. Slides a
|
||||||
|
* fixed-second window across each group's events; a window with at least
|
||||||
|
* THRESHOLD_COUNT positive-delta events triggers a problem.
|
||||||
|
*
|
||||||
|
* Negative-delta events (drops, transfers out) are ignored — they do not
|
||||||
|
* indicate creation of items and a sufficiently fast trade-and-pickup loop
|
||||||
|
* would self-cancel.
|
||||||
|
*
|
||||||
|
* Entry::getTime() resolves to integer Unix seconds, so sub-second
|
||||||
|
* timestamps in the fixture all collapse to the same value. This is
|
||||||
|
* acceptable for v1: events emitted within the same second are by
|
||||||
|
* definition within any positive window.
|
||||||
|
*/
|
||||||
|
class ItemDuplicationAnalyser extends Analyser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Minimum number of same-item gain events that must fall inside the
|
||||||
|
* window before a Problem is emitted. Five was picked because legitimate
|
||||||
|
* gameplay rarely produces five identical items in ten seconds:
|
||||||
|
* crafting has animation delays, looting is one-at-a-time, and zombie
|
||||||
|
* drops are similarly serial. A burst of five suggests admin-spawn or
|
||||||
|
* exploit. Tune downward if false negatives appear in production logs.
|
||||||
|
*/
|
||||||
|
public const int THRESHOLD_COUNT = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length of the sliding window in seconds. Ten seconds covers a
|
||||||
|
* realistic burst-loot scenario (e.g. crate of identical items) without
|
||||||
|
* collapsing onto unrelated events. Combined with THRESHOLD_COUNT this
|
||||||
|
* means an effective rate of 0.5 same-item events per second.
|
||||||
|
*/
|
||||||
|
public const int THRESHOLD_WINDOW_SECONDS = 10;
|
||||||
|
|
||||||
|
public function analyse(): AnalysisInterface
|
||||||
|
{
|
||||||
|
$analysis = new Analysis();
|
||||||
|
$analysis->setLog($this->log);
|
||||||
|
|
||||||
|
$groups = [];
|
||||||
|
foreach ($this->log as $entry) {
|
||||||
|
if (preg_match(ItemPattern::FIELDS, (string) $entry, $m) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_starts_with($m['delta'], '+')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$key = $m['steamid'] . '|' . $m['item'];
|
||||||
|
$groups[$key][] = [
|
||||||
|
'time' => $entry->getTime() ?? 0,
|
||||||
|
'steamid' => $m['steamid'],
|
||||||
|
'item' => $m['item'],
|
||||||
|
'player' => $m['player'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($groups as $events) {
|
||||||
|
usort($events, static fn($a, $b) => $a['time'] <=> $b['time']);
|
||||||
|
|
||||||
|
$left = 0;
|
||||||
|
$eventCount = count($events);
|
||||||
|
for ($right = 0; $right < $eventCount; $right++) {
|
||||||
|
while ($events[$right]['time'] - $events[$left]['time'] > self::THRESHOLD_WINDOW_SECONDS) {
|
||||||
|
$left++;
|
||||||
|
}
|
||||||
|
if (($right - $left + 1) >= self::THRESHOLD_COUNT) {
|
||||||
|
$sample = $events[0];
|
||||||
|
$analysis->addInsight((new ItemDuplicationProblem())
|
||||||
|
->setSteamId($sample['steamid'])
|
||||||
|
->setPlayer($sample['player'])
|
||||||
|
->setItem($sample['item'])
|
||||||
|
->setEventCount($eventCount));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $analysis;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analyser\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\Analyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Analysis;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\SkillProgressionAnomalyProblem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\PerkPattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks PerkLog entries, parses each perks-snapshot row into a
|
||||||
|
* skill->level dict, and compares consecutive snapshots per Steam ID. If
|
||||||
|
* any single skill gained more than THRESHOLD_DELTA levels between
|
||||||
|
* snapshots, emits a SkillProgressionAnomalyProblem for that
|
||||||
|
* (player, skill) pair.
|
||||||
|
*
|
||||||
|
* Login/Logout/LevelUp event rows are skipped — they have a single token
|
||||||
|
* in the event field rather than a comma-separated list of Skill=N pairs.
|
||||||
|
*/
|
||||||
|
class SkillProgressionAnomalyAnalyser extends Analyser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Maximum plausible single-skill gain between two consecutive snapshots
|
||||||
|
* of the same player. Project Zomboid skill leveling is slow: most
|
||||||
|
* skills require thousands of XP per level, and even maxed grinding
|
||||||
|
* setups don't routinely produce four-or-more level jumps in a single
|
||||||
|
* session bridge. Set to 3 as a baseline; if production logs surface
|
||||||
|
* frequent legitimate jumps of 4 (e.g. on heavily modded XP servers),
|
||||||
|
* raise via subclass override or tune downward to catch finer abuse.
|
||||||
|
*/
|
||||||
|
public const int THRESHOLD_DELTA = 3;
|
||||||
|
|
||||||
|
public function analyse(): AnalysisInterface
|
||||||
|
{
|
||||||
|
$analysis = new Analysis();
|
||||||
|
$analysis->setLog($this->log);
|
||||||
|
|
||||||
|
$snapshots = [];
|
||||||
|
foreach ($this->log as $entry) {
|
||||||
|
$text = (string) $entry;
|
||||||
|
if (preg_match(PerkPattern::FIELDS, $text, $m) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match(PerkPattern::PERK_PAIR, $m['event']) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
preg_match_all(PerkPattern::PERK_PAIR, $m['event'], $pairs, PREG_SET_ORDER);
|
||||||
|
$skills = [];
|
||||||
|
foreach ($pairs as $pair) {
|
||||||
|
$skills[$pair['skill']] = (int) $pair['level'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshots[$m['steamid']][] = [
|
||||||
|
'time' => $entry->getTime() ?? 0,
|
||||||
|
'player' => $m['player'],
|
||||||
|
'skills' => $skills,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($snapshots as $steamId => $playerSnapshots) {
|
||||||
|
usort($playerSnapshots, static fn($a, $b) => $a['time'] <=> $b['time']);
|
||||||
|
|
||||||
|
for ($i = 1; $i < count($playerSnapshots); $i++) {
|
||||||
|
$prev = $playerSnapshots[$i - 1];
|
||||||
|
$curr = $playerSnapshots[$i];
|
||||||
|
|
||||||
|
foreach ($curr['skills'] as $skill => $currLevel) {
|
||||||
|
$prevLevel = $prev['skills'][$skill] ?? 0;
|
||||||
|
$delta = $currLevel - $prevLevel;
|
||||||
|
if ($delta > self::THRESHOLD_DELTA) {
|
||||||
|
$analysis->addInsight((new SkillProgressionAnomalyProblem())
|
||||||
|
->setSteamId($steamId)
|
||||||
|
->setPlayer($curr['player'])
|
||||||
|
->setSkill($skill)
|
||||||
|
->setFromLevel($prevLevel)
|
||||||
|
->setToLevel($currLevel)
|
||||||
|
->setDelta($delta));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $analysis;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Analyser/SevenDaysToDie/.gitkeep
Normal file
0
src/Analyser/SevenDaysToDie/.gitkeep
Normal file
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Analysis
|
* Class Analysis
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
class Analysis implements AnalysisInterface
|
class Analysis implements AnalysisInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
use Countable;
|
use Countable;
|
||||||
use Iterator;
|
use Iterator;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
@@ -11,7 +11,7 @@ use JsonSerializable;
|
|||||||
/**
|
/**
|
||||||
* Interface AnalysisInterface
|
* Interface AnalysisInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface AnalysisInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
interface AnalysisInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface AutomatableSolutionInterface
|
* Interface AutomatableSolutionInterface
|
||||||
@@ -9,7 +9,7 @@ namespace Aternos\Codex\Analysis;
|
|||||||
* that a solution can be solved automatically
|
* that a solution can be solved automatically
|
||||||
* e.g. deletion/creation/modification of files
|
* e.g. deletion/creation/modification of files
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface AutomatableSolutionInterface extends SolutionInterface
|
interface AutomatableSolutionInterface extends SolutionInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Information
|
* Class Information
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
abstract class Information extends Insight implements InformationInterface
|
abstract class Information extends Insight implements InformationInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface InformationInterface
|
* Interface InformationInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface InformationInterface extends InsightInterface
|
interface InformationInterface extends InsightInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use Aternos\Codex\Log\EntryInterface;
|
use IndifferentKetchup\Codex\Log\EntryInterface;
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Insight
|
* Class Insight
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
abstract class Insight implements InsightInterface
|
abstract class Insight implements InsightInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use Aternos\Codex\Log\EntryInterface;
|
use IndifferentKetchup\Codex\Log\EntryInterface;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface InsightInterface
|
* Interface InsightInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface InsightInterface extends JsonSerializable
|
interface InsightInterface extends JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface PatternInsightInterface
|
* Interface PatternInsightInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface PatternInsightInterface extends InsightInterface
|
interface PatternInsightInterface extends InsightInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Problem
|
* Class Problem
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
abstract class Problem extends Insight implements ProblemInterface
|
abstract class Problem extends Insight implements ProblemInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use Countable;
|
use Countable;
|
||||||
@@ -9,7 +9,7 @@ use Iterator;
|
|||||||
/**
|
/**
|
||||||
* Interface ProblemInterface
|
* Interface ProblemInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface ProblemInterface extends Iterator, Countable, ArrayAccess, InsightInterface
|
interface ProblemInterface extends Iterator, Countable, ArrayAccess, InsightInterface
|
||||||
{
|
{
|
||||||
|
|||||||
26
src/Analysis/ProjectZomboid/AdminAddedItemInformation.php
Normal file
26
src/Analysis/ProjectZomboid/AdminAddedItemInformation.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminAddedItemInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::ADDED_ITEM_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin added item');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s added %s to %s',
|
||||||
|
$matches['admin'],
|
||||||
|
$matches['item'],
|
||||||
|
$matches['target']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Analysis/ProjectZomboid/AdminAddedXpInformation.php
Normal file
27
src/Analysis/ProjectZomboid/AdminAddedXpInformation.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminAddedXpInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::ADDED_XP_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin added xp');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s added %s %s xp to %s',
|
||||||
|
$matches['admin'],
|
||||||
|
$matches['amount'],
|
||||||
|
$matches['skill'],
|
||||||
|
$matches['target']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminChangedOptionInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::CHANGED_OPTION_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin changed option');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s set %s=%s',
|
||||||
|
$matches['admin'],
|
||||||
|
$matches['option'],
|
||||||
|
$matches['value']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminGrantedAccessInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::GRANTED_ACCESS_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin granted access');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s granted %s to %s',
|
||||||
|
$matches['admin'],
|
||||||
|
$matches['level'],
|
||||||
|
$matches['target']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminReloadedOptionsInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::RELOADED_OPTIONS_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin reloaded options');
|
||||||
|
$this->setValue($matches['admin']);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Analysis/ProjectZomboid/AdminTeleportedInformation.php
Normal file
28
src/Analysis/ProjectZomboid/AdminTeleportedInformation.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\AdminPattern;
|
||||||
|
|
||||||
|
class AdminTeleportedInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [AdminPattern::TELEPORTED_ENTRY];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Admin teleported');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s teleported %s to %s,%s,%s',
|
||||||
|
$matches['admin'],
|
||||||
|
$matches['target'],
|
||||||
|
$matches['x'],
|
||||||
|
$matches['y'],
|
||||||
|
$matches['z']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Analysis/ProjectZomboid/ConnectionFailureProblem.php
Normal file
67
src/Analysis/ProjectZomboid/ConnectionFailureProblem.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problem emitted by ConnectionFailureAnalyser when a player's
|
||||||
|
* "attempting to join" event count exceeds their "allowed to join" count
|
||||||
|
* within the same log file. Coalesced by Steam ID so each player produces
|
||||||
|
* at most one problem regardless of how many unmatched attempts they have.
|
||||||
|
*/
|
||||||
|
class ConnectionFailureProblem extends Problem
|
||||||
|
{
|
||||||
|
private string $steamId = '';
|
||||||
|
private string $player = '';
|
||||||
|
private int $unmatchedAttempts = 0;
|
||||||
|
|
||||||
|
public function setSteamId(string $steamId): static
|
||||||
|
{
|
||||||
|
$this->steamId = $steamId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayer(string $player): static
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUnmatchedAttempts(int $count): static
|
||||||
|
{
|
||||||
|
$this->unmatchedAttempts = $count;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSteamId(): string
|
||||||
|
{
|
||||||
|
return $this->steamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayer(): string
|
||||||
|
{
|
||||||
|
return $this->player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnmatchedAttempts(): int
|
||||||
|
{
|
||||||
|
return $this->unmatchedAttempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Player %s (%s) had %d "attempting to join" event(s) without a matching "allowed to join".',
|
||||||
|
$this->player,
|
||||||
|
$this->steamId,
|
||||||
|
$this->unmatchedAttempts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self && $insight->getSteamId() === $this->steamId;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Analysis/ProjectZomboid/EngineVersionInformation.php
Normal file
27
src/Analysis/ProjectZomboid/EngineVersionInformation.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class EngineVersionInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::VERSION];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Engine version');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s (build %s, %s %s)',
|
||||||
|
$matches['version'],
|
||||||
|
$matches['hash'],
|
||||||
|
$matches['date'],
|
||||||
|
$matches['time']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/Analysis/ProjectZomboid/ItemDuplicationProblem.php
Normal file
82
src/Analysis/ProjectZomboid/ItemDuplicationProblem.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problem emitted by ItemDuplicationAnalyser when a player gains the same
|
||||||
|
* item code at a rate that exceeds the configured threshold. Coalesced by
|
||||||
|
* the (Steam ID, item code) tuple so each suspicious group produces one
|
||||||
|
* problem regardless of how many events fall inside the window.
|
||||||
|
*/
|
||||||
|
class ItemDuplicationProblem extends Problem
|
||||||
|
{
|
||||||
|
private string $steamId = '';
|
||||||
|
private string $player = '';
|
||||||
|
private string $item = '';
|
||||||
|
private int $eventCount = 0;
|
||||||
|
|
||||||
|
public function setSteamId(string $steamId): static
|
||||||
|
{
|
||||||
|
$this->steamId = $steamId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayer(string $player): static
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setItem(string $item): static
|
||||||
|
{
|
||||||
|
$this->item = $item;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEventCount(int $count): static
|
||||||
|
{
|
||||||
|
$this->eventCount = $count;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSteamId(): string
|
||||||
|
{
|
||||||
|
return $this->steamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayer(): string
|
||||||
|
{
|
||||||
|
return $this->player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItem(): string
|
||||||
|
{
|
||||||
|
return $this->item;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEventCount(): int
|
||||||
|
{
|
||||||
|
return $this->eventCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Player %s (%s) gained %s %d times at a rate above the duplication threshold.',
|
||||||
|
$this->player,
|
||||||
|
$this->steamId,
|
||||||
|
$this->item,
|
||||||
|
$this->eventCount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self
|
||||||
|
&& $insight->getSteamId() === $this->steamId
|
||||||
|
&& $insight->getItem() === $this->item;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Analysis/ProjectZomboid/ModLoadInformation.php
Normal file
21
src/Analysis/ProjectZomboid/ModLoadInformation.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ModLoadInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::MOD_LOAD];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('Mod loaded');
|
||||||
|
$this->setValue($matches['mod']);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Analysis/ProjectZomboid/ModMissingProblem.php
Normal file
39
src/Analysis/ProjectZomboid/ModMissingProblem.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ModMissingProblem extends Problem implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
private string $modName = '';
|
||||||
|
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::MOD_MISSING];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->modName = $matches['mod'];
|
||||||
|
$this->addSolution((new ModMissingSolution())->setModName($this->modName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModName(): string
|
||||||
|
{
|
||||||
|
return $this->modName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf('Required mod "%s" not found.', $this->modName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self && $insight->getModName() === $this->modName;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/Analysis/ProjectZomboid/ModMissingSolution.php
Normal file
24
src/Analysis/ProjectZomboid/ModMissingSolution.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Solution;
|
||||||
|
|
||||||
|
class ModMissingSolution extends Solution
|
||||||
|
{
|
||||||
|
private string $modName = '';
|
||||||
|
|
||||||
|
public function setModName(string $modName): static
|
||||||
|
{
|
||||||
|
$this->modName = $modName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Subscribe to mod "%s" or remove its ID from the Mods= line in serverconfig.ini.',
|
||||||
|
$this->modName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Analysis/ProjectZomboid/PvpDamageInformation.php
Normal file
26
src/Analysis/ProjectZomboid/PvpDamageInformation.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Information;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\PvpPattern;
|
||||||
|
|
||||||
|
class PvpDamageInformation extends Information implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [PvpPattern::COMBAT_REAL];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->setLabel('PvP combat');
|
||||||
|
$this->setValue(sprintf(
|
||||||
|
'%s hit %s with %s',
|
||||||
|
$matches['attacker'],
|
||||||
|
$matches['victim'],
|
||||||
|
$matches['weapon']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Analysis/ProjectZomboid/ServerExceptionProblem.php
Normal file
46
src/Analysis/ProjectZomboid/ServerExceptionProblem.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\PatternInsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
use IndifferentKetchup\Codex\Pattern\ProjectZomboid\DebugServerPattern;
|
||||||
|
|
||||||
|
class ServerExceptionProblem extends Problem implements PatternInsightInterface
|
||||||
|
{
|
||||||
|
private string $exceptionType = '';
|
||||||
|
private string $body = '';
|
||||||
|
|
||||||
|
public static function getPatterns(): array
|
||||||
|
{
|
||||||
|
return [DebugServerPattern::EXCEPTION];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMatches(array $matches, mixed $patternKey): void
|
||||||
|
{
|
||||||
|
$this->exceptionType = $matches['type'];
|
||||||
|
$this->body = trim($matches['body'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExceptionType(): string
|
||||||
|
{
|
||||||
|
return $this->exceptionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBody(): string
|
||||||
|
{
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf('Exception thrown: %s', $this->exceptionType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self
|
||||||
|
&& $insight->getExceptionType() === $this->exceptionType;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/Analysis/ProjectZomboid/SkillProgressionAnomalyProblem.php
Normal file
107
src/Analysis/ProjectZomboid/SkillProgressionAnomalyProblem.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\InsightInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\Problem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Problem emitted by SkillProgressionAnomalyAnalyser when a single skill
|
||||||
|
* gained more than the configured threshold between two consecutive
|
||||||
|
* snapshots of the same player. Coalesced by (Steam ID, skill).
|
||||||
|
*/
|
||||||
|
class SkillProgressionAnomalyProblem extends Problem
|
||||||
|
{
|
||||||
|
private string $steamId = '';
|
||||||
|
private string $player = '';
|
||||||
|
private string $skill = '';
|
||||||
|
private int $fromLevel = 0;
|
||||||
|
private int $toLevel = 0;
|
||||||
|
private int $delta = 0;
|
||||||
|
|
||||||
|
public function setSteamId(string $steamId): static
|
||||||
|
{
|
||||||
|
$this->steamId = $steamId;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPlayer(string $player): static
|
||||||
|
{
|
||||||
|
$this->player = $player;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSkill(string $skill): static
|
||||||
|
{
|
||||||
|
$this->skill = $skill;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFromLevel(int $level): static
|
||||||
|
{
|
||||||
|
$this->fromLevel = $level;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setToLevel(int $level): static
|
||||||
|
{
|
||||||
|
$this->toLevel = $level;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDelta(int $delta): static
|
||||||
|
{
|
||||||
|
$this->delta = $delta;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSteamId(): string
|
||||||
|
{
|
||||||
|
return $this->steamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPlayer(): string
|
||||||
|
{
|
||||||
|
return $this->player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSkill(): string
|
||||||
|
{
|
||||||
|
return $this->skill;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFromLevel(): int
|
||||||
|
{
|
||||||
|
return $this->fromLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getToLevel(): int
|
||||||
|
{
|
||||||
|
return $this->toLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDelta(): int
|
||||||
|
{
|
||||||
|
return $this->delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return sprintf(
|
||||||
|
'Player %s (%s) gained %d levels of %s between snapshots (%d to %d).',
|
||||||
|
$this->player,
|
||||||
|
$this->steamId,
|
||||||
|
$this->delta,
|
||||||
|
$this->skill,
|
||||||
|
$this->fromLevel,
|
||||||
|
$this->toLevel
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEqual(InsightInterface $insight): bool
|
||||||
|
{
|
||||||
|
return $insight instanceof self
|
||||||
|
&& $insight->getSteamId() === $this->steamId
|
||||||
|
&& $insight->getSkill() === $this->skill;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Solution
|
* Class Solution
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
abstract class Solution implements SolutionInterface
|
abstract class Solution implements SolutionInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Analysis;
|
namespace IndifferentKetchup\Codex\Analysis;
|
||||||
|
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface SolutionInterface
|
* Interface SolutionInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Analysis
|
* @package IndifferentKetchup\Codex\Analysis
|
||||||
*/
|
*/
|
||||||
interface SolutionInterface extends JsonSerializable
|
interface SolutionInterface extends JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
use Aternos\Codex\Log\DetectableLogInterface;
|
use IndifferentKetchup\Codex\Log\DetectableLogInterface;
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
use Aternos\Codex\Log\Log;
|
use IndifferentKetchup\Codex\Log\Log;
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Detective
|
* Class Detective
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
class Detective implements DetectiveInterface
|
class Detective implements DetectiveInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface DetectiveInterface
|
* Interface DetectiveInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
interface DetectiveInterface
|
interface DetectiveInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Detector
|
* Class Detector
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
abstract class Detector implements DetectorInterface
|
abstract class Detector implements DetectorInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface DetectorInterface
|
* Interface DetectorInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
interface DetectorInterface
|
interface DetectorInterface
|
||||||
{
|
{
|
||||||
|
|||||||
45
src/Detective/FilenameDetector.php
Normal file
45
src/Detective/FilenameDetector.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a regex against the source path of the log file
|
||||||
|
*
|
||||||
|
* Returns a configured weight when the LogFileInterface exposes a non-null
|
||||||
|
* path that matches $pattern. Returns false when no path is known
|
||||||
|
* (StringLogFile, StreamLogFile) or when the pattern does not match. Pattern
|
||||||
|
* is compared against the full path; anchor with $ for strict suffix matching.
|
||||||
|
*
|
||||||
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
|
*/
|
||||||
|
class FilenameDetector extends Detector
|
||||||
|
{
|
||||||
|
protected ?string $pattern = null;
|
||||||
|
protected float $weight = 0.95;
|
||||||
|
|
||||||
|
public function setPattern(string $pattern): static
|
||||||
|
{
|
||||||
|
$this->pattern = $pattern;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWeight(float $weight): static
|
||||||
|
{
|
||||||
|
$this->weight = $weight;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function detect(): bool|float
|
||||||
|
{
|
||||||
|
$path = $this->logFile->getPath();
|
||||||
|
if ($path === null || $this->pattern === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($this->pattern, $path) === 1) {
|
||||||
|
return $this->weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Detective/Hytale/HytaleDetective.php
Normal file
10
src/Detective/Hytale/HytaleDetective.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Detective\Hytale;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Detective\Detective;
|
||||||
|
|
||||||
|
class HytaleDetective extends Detective
|
||||||
|
{
|
||||||
|
// TODO: implement game-specific log type detection
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LinePatternDetector
|
* Class LinePatternDetector
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
class LinePatternDetector extends PatternDetector
|
class LinePatternDetector extends PatternDetector
|
||||||
{
|
{
|
||||||
|
|||||||
10
src/Detective/Minecraft/MinecraftDetective.php
Normal file
10
src/Detective/Minecraft/MinecraftDetective.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Detective\Minecraft;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Detective\Detective;
|
||||||
|
|
||||||
|
class MinecraftDetective extends Detective
|
||||||
|
{
|
||||||
|
// TODO: implement game-specific log type detection
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MultiPatternDetector can detect multiple patterns in a log and return true if all patterns are found
|
* MultiPatternDetector can detect multiple patterns in a log and return true if all patterns are found
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PatternDetector
|
* Class PatternDetector
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
abstract class PatternDetector extends Detector
|
abstract class PatternDetector extends Detector
|
||||||
{
|
{
|
||||||
|
|||||||
38
src/Detective/ProjectZomboid/ProjectZomboidDetective.php
Normal file
38
src/Detective/ProjectZomboid/ProjectZomboidDetective.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Detective\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Detective\Detective;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidAdminLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidBurdJournalsLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidChatLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidClientActionLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidCmdLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidItemLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidMapLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidPerkLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidPvpLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidServerLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidUserLog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-registers all eleven ProjectZomboid log classes so that detect()
|
||||||
|
* can dispatch among them on filename hint + content signature.
|
||||||
|
*/
|
||||||
|
class ProjectZomboidDetective extends Detective
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidServerLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidChatLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidClientActionLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidCmdLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidItemLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidMapLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidPerkLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidPvpLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidAdminLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidUserLog::class);
|
||||||
|
$this->addPossibleLogClass(ProjectZomboidBurdJournalsLog::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/Detective/SevenDaysToDie/SevenDaysToDieDetective.php
Normal file
10
src/Detective/SevenDaysToDie/SevenDaysToDieDetective.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Detective\SevenDaysToDie;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Detective\Detective;
|
||||||
|
|
||||||
|
class SevenDaysToDieDetective extends Detective
|
||||||
|
{
|
||||||
|
// TODO: implement game-specific log type detection
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class SinglePatternDetector
|
* Class SinglePatternDetector
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
class SinglePatternDetector extends PatternDetector
|
class SinglePatternDetector extends PatternDetector
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Detective;
|
namespace IndifferentKetchup\Codex\Detective;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class WeightedSinglePatternDetector
|
* Class WeightedSinglePatternDetector
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Detective
|
* @package IndifferentKetchup\Codex\Detective
|
||||||
*/
|
*/
|
||||||
class WeightedSinglePatternDetector extends SinglePatternDetector
|
class WeightedSinglePatternDetector extends SinglePatternDetector
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use Aternos\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use Aternos\Codex\Analysis\AnalysisInterface;
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class AnalysableLog
|
* Class AnalysableLog
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
abstract class AnalysableLog extends Log implements AnalysableLogInterface
|
abstract class AnalysableLog extends Log implements AnalysableLogInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use Aternos\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use Aternos\Codex\Analysis\AnalysisInterface;
|
use IndifferentKetchup\Codex\Analysis\AnalysisInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface AnalysableLogInterface
|
* Interface AnalysableLogInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
interface AnalysableLogInterface
|
interface AnalysableLogInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use Aternos\Codex\Detective\DetectorInterface;
|
use IndifferentKetchup\Codex\Detective\DetectorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface DetectableLogInterface
|
* Interface DetectableLogInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
interface DetectableLogInterface extends LogInterface
|
interface DetectableLogInterface extends LogInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Entry
|
* Class Entry
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
class Entry implements EntryInterface
|
class Entry implements EntryInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use Countable;
|
use Countable;
|
||||||
@@ -10,7 +10,7 @@ use JsonSerializable;
|
|||||||
/**
|
/**
|
||||||
* Interface EntryInterface
|
* Interface EntryInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
interface EntryInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
interface EntryInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log\File;
|
namespace IndifferentKetchup\Codex\Log\File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LogFile
|
* Class LogFile
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log\File
|
* @package IndifferentKetchup\Codex\Log\File
|
||||||
*/
|
*/
|
||||||
abstract class LogFile implements LogFileInterface
|
abstract class LogFile implements LogFileInterface
|
||||||
{
|
{
|
||||||
protected ?string $content = null;
|
protected ?string $content = null;
|
||||||
|
protected ?string $path = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the log file content
|
* Get the log file content
|
||||||
@@ -20,4 +21,14 @@ abstract class LogFile implements LogFileInterface
|
|||||||
{
|
{
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the source path of the log file when one is known
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getPath(): ?string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log\File;
|
namespace IndifferentKetchup\Codex\Log\File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface LogFileInterface
|
* Interface LogFileInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log\File
|
* @package IndifferentKetchup\Codex\Log\File
|
||||||
*/
|
*/
|
||||||
interface LogFileInterface
|
interface LogFileInterface
|
||||||
{
|
{
|
||||||
@@ -15,4 +15,15 @@ interface LogFileInterface
|
|||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getContent(): string;
|
public function getContent(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the source path of the log file when one is known
|
||||||
|
*
|
||||||
|
* Returns null for log files without a filesystem origin (string content,
|
||||||
|
* arbitrary streams). Concrete implementations should return the path used
|
||||||
|
* to construct them when applicable.
|
||||||
|
*
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
public function getPath(): ?string;
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log\File;
|
namespace IndifferentKetchup\Codex\Log\File;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PathLogFile
|
* Class PathLogFile
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log\File
|
* @package IndifferentKetchup\Codex\Log\File
|
||||||
*/
|
*/
|
||||||
class PathLogFile extends LogFile
|
class PathLogFile extends LogFile
|
||||||
{
|
{
|
||||||
@@ -22,6 +22,7 @@ class PathLogFile extends LogFile
|
|||||||
throw new InvalidArgumentException("File '" . $path . "' not found.");
|
throw new InvalidArgumentException("File '" . $path . "' not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->path = $path;
|
||||||
$this->content = file_get_contents($path);
|
$this->content = file_get_contents($path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log\File;
|
namespace IndifferentKetchup\Codex\Log\File;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class StreamLogFile
|
* Class StreamLogFile
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log\File
|
* @package IndifferentKetchup\Codex\Log\File
|
||||||
*/
|
*/
|
||||||
class StreamLogFile extends LogFile
|
class StreamLogFile extends LogFile
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log\File;
|
namespace IndifferentKetchup\Codex\Log\File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class StringLogFile
|
* Class StringLogFile
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log\File
|
* @package IndifferentKetchup\Codex\Log\File
|
||||||
*/
|
*/
|
||||||
class StringLogFile extends LogFile
|
class StringLogFile extends LogFile
|
||||||
{
|
{
|
||||||
|
|||||||
0
src/Log/Hytale/.gitkeep
Normal file
0
src/Log/Hytale/.gitkeep
Normal file
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
enum Level: int implements LevelInterface
|
enum Level: int implements LevelInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Line
|
* Class Line
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
class Line implements LineInterface
|
class Line implements LineInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface LineInterface
|
* Interface LineInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
interface LineInterface extends JsonSerializable
|
interface LineInterface extends JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
use Aternos\Codex\Parser\DefaultParser;
|
use IndifferentKetchup\Codex\Parser\DefaultParser;
|
||||||
use Aternos\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Log
|
* Class Log
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
class Log implements LogInterface
|
class Log implements LogInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Log;
|
namespace IndifferentKetchup\Codex\Log;
|
||||||
|
|
||||||
use ArrayAccess;
|
use ArrayAccess;
|
||||||
use Aternos\Codex\Log\File\LogFileInterface;
|
use IndifferentKetchup\Codex\Log\File\LogFileInterface;
|
||||||
use Aternos\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
use Countable;
|
use Countable;
|
||||||
use Iterator;
|
use Iterator;
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
@@ -12,7 +12,7 @@ use JsonSerializable;
|
|||||||
/**
|
/**
|
||||||
* Interface LogInterface
|
* Interface LogInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Log
|
* @package IndifferentKetchup\Codex\Log
|
||||||
*/
|
*/
|
||||||
interface LogInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
interface LogInterface extends Iterator, Countable, ArrayAccess, JsonSerializable
|
||||||
{
|
{
|
||||||
|
|||||||
0
src/Log/Minecraft/.gitkeep
Normal file
0
src/Log/Minecraft/.gitkeep
Normal file
59
src/Log/ProjectZomboid/ProjectZomboidAdminLog.php
Normal file
59
src/Log/ProjectZomboid/ProjectZomboidAdminLog.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminAddedItemInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminAddedXpInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminChangedOptionInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminGrantedAccessInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminReloadedOptionsInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\AdminTeleportedInformation;
|
||||||
|
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\AdminPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidAdminLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
AdminPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(AdminAddedItemInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminAddedXpInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminGrantedAccessInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminChangedOptionInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminReloadedOptionsInformation::class)
|
||||||
|
->addPossibleInsightClass(AdminTeleportedInformation::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_admin\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] .+? added item Base\.\S+ in .+?\'s inventory/m')
|
||||||
|
->setWeight(0.90),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] .+? granted (?:admin|user|moderator|gm|observer) access level on /m')
|
||||||
|
->setWeight(0.85),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Admin Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidBurdJournalsLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidBurdJournalsLog.php
Normal 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\BurdJournalsPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidBurdJournalsLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
BurdJournalsPattern::LINE,
|
||||||
|
[PatternParser::TIME, PatternParser::PREFIX, PatternParser::LEVEL]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new PatternAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_BurdJournals\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/\[BurdJournals\]/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid BurdJournals Mod Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/Log/ProjectZomboid/ProjectZomboidChatLog.php
Normal file
47
src/Log/ProjectZomboid/ProjectZomboidChatLog.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?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\ChatPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidChatLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
ChatPattern::LINE,
|
||||||
|
[PatternParser::TIME, PatternParser::LEVEL]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new PatternAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_chat\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/Got message:ChatMessage\{chat=\w+/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/Start chat server initialization/')
|
||||||
|
->setWeight(0.85),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Chat Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidClientActionLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidClientActionLog.php
Normal 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\ClientActionPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidClientActionLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
ClientActionPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new PatternAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_ClientActionLog\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/\[\d{17}\]\[(?:ISEnterVehicle|ISExitVehicle|ISWalkToTimedAction)\]\[/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Client Action Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidCmdLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidCmdLog.php
Normal 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\CmdPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidCmdLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
CmdPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new PatternAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_cmd\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] \d{17} "[^"]+" \w[\w.]+ @ \d/m')
|
||||||
|
->setWeight(0.85),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Command Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Log/ProjectZomboid/ProjectZomboidEventLog.php
Normal file
14
src/Log/ProjectZomboid/ProjectZomboidEventLog.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker base for ProjectZomboid logs whose entries are strictly one line
|
||||||
|
* each (the ten structured event files: admin, BurdJournals, chat,
|
||||||
|
* ClientActionLog, cmd, item, map, PerkLog, pvp, user). Distinct from
|
||||||
|
* ProjectZomboidServerLog, which permits multi-line entries
|
||||||
|
* (DebugLog-server stack traces).
|
||||||
|
*/
|
||||||
|
abstract class ProjectZomboidEventLog extends ProjectZomboidLog
|
||||||
|
{
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidItemLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidItemLog.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ItemDuplicationAnalyser;
|
||||||
|
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\ItemPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidItemLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
ItemPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new ItemDuplicationAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_item\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] \d{17} "[^"]+" (?:container|floor|inventory) [+\-]\d+ /m')
|
||||||
|
->setWeight(0.90),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Item Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/Log/ProjectZomboid/ProjectZomboidLog.php
Normal file
31
src/Log/ProjectZomboid/ProjectZomboidLog.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use DateTimeZone;
|
||||||
|
use IndifferentKetchup\Codex\Log\AnalysableLog;
|
||||||
|
use IndifferentKetchup\Codex\Log\DetectableLogInterface;
|
||||||
|
use IndifferentKetchup\Codex\Parser\PatternParser;
|
||||||
|
|
||||||
|
abstract class ProjectZomboidLog extends AnalysableLog implements DetectableLogInterface
|
||||||
|
{
|
||||||
|
public const string TIME_FORMAT = 'd-m-y H:i:s.v';
|
||||||
|
public const string DEFAULT_TIMEZONE = 'UTC';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a PatternParser preconfigured with the shared PZ time format
|
||||||
|
* and timezone. Subclasses pass their line regex and the names of the
|
||||||
|
* capture groups by index.
|
||||||
|
*
|
||||||
|
* @param string $pattern PCRE regex anchored at line start, with named groups
|
||||||
|
* @param array<int, string> $matches Match-type constants in capture-group order
|
||||||
|
*/
|
||||||
|
protected static function makePatternParser(string $pattern, array $matches): PatternParser
|
||||||
|
{
|
||||||
|
return (new PatternParser())
|
||||||
|
->setPattern($pattern)
|
||||||
|
->setMatches($matches)
|
||||||
|
->setTimeFormat(static::TIME_FORMAT)
|
||||||
|
->setTimezone(new DateTimeZone(static::DEFAULT_TIMEZONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidMapLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidMapLog.php
Normal 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\MapPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidMapLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
MapPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new PatternAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_map\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] \d{17} "[^"]+" (?:added|removed) (?:Base\.|IsoObject )/m')
|
||||||
|
->setWeight(0.90),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid Map Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Log/ProjectZomboid/ProjectZomboidPerkLog.php
Normal file
44
src/Log/ProjectZomboid/ProjectZomboidPerkLog.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\SkillProgressionAnomalyAnalyser;
|
||||||
|
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 SkillProgressionAnomalyAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/Log/ProjectZomboid/ProjectZomboidPvpLog.php
Normal file
49
src/Log/ProjectZomboid/ProjectZomboidPvpLog.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\PvpDamageInformation;
|
||||||
|
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\PvpPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidPvpLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
PvpPattern::LINE,
|
||||||
|
[PatternParser::TIME, PatternParser::LEVEL, PatternParser::PREFIX]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return (new PatternAnalyser())
|
||||||
|
->addPossibleInsightClass(PvpDamageInformation::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->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";
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/Log/ProjectZomboid/ProjectZomboidServerLog.php
Normal file
62
src/Log/ProjectZomboid/ProjectZomboidServerLog.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\EngineVersionInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModLoadInformation;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ModMissingProblem;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ServerExceptionProblem;
|
||||||
|
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())
|
||||||
|
->addPossibleInsightClass(EngineVersionInformation::class)
|
||||||
|
->addPossibleInsightClass(ModLoadInformation::class)
|
||||||
|
->addPossibleInsightClass(ModMissingProblem::class)
|
||||||
|
->addPossibleInsightClass(ServerExceptionProblem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/Log/ProjectZomboid/ProjectZomboidUserLog.php
Normal file
47
src/Log/ProjectZomboid/ProjectZomboidUserLog.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ConnectionFailureAnalyser;
|
||||||
|
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\UserPattern;
|
||||||
|
|
||||||
|
class ProjectZomboidUserLog extends ProjectZomboidEventLog
|
||||||
|
{
|
||||||
|
public static function getDefaultParser(): ParserInterface
|
||||||
|
{
|
||||||
|
return static::makePatternParser(
|
||||||
|
UserPattern::LINE,
|
||||||
|
[PatternParser::TIME]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
|
{
|
||||||
|
return new ConnectionFailureAnalyser();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getDetectors(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(new FilenameDetector())
|
||||||
|
->setPattern('/_user\.txt$/')
|
||||||
|
->setWeight(0.95),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] Connection (?:add|disconnect) index=\d+ guid=\d+/m')
|
||||||
|
->setWeight(0.90),
|
||||||
|
(new WeightedSinglePatternDetector())
|
||||||
|
->setPattern('/^\[[^\]]+\] \d{17} "[^"]+" (?:attempting to join|allowed to join)/m')
|
||||||
|
->setWeight(0.85),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return "Project Zomboid User Log";
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/Log/SevenDaysToDie/.gitkeep
Normal file
0
src/Log/SevenDaysToDie/.gitkeep
Normal file
@@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Parser;
|
namespace IndifferentKetchup\Codex\Parser;
|
||||||
|
|
||||||
use Aternos\Codex\Log\Entry;
|
use IndifferentKetchup\Codex\Log\Entry;
|
||||||
use Aternos\Codex\Log\Line;
|
use IndifferentKetchup\Codex\Log\Line;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class DefaultParser
|
* Class DefaultParser
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Parser
|
* @package IndifferentKetchup\Codex\Parser
|
||||||
*/
|
*/
|
||||||
class DefaultParser extends Parser
|
class DefaultParser extends Parser
|
||||||
{
|
{
|
||||||
|
|||||||
0
src/Parser/Hytale/.gitkeep
Normal file
0
src/Parser/Hytale/.gitkeep
Normal file
0
src/Parser/Minecraft/.gitkeep
Normal file
0
src/Parser/Minecraft/.gitkeep
Normal file
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Parser;
|
namespace IndifferentKetchup\Codex\Parser;
|
||||||
|
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class Parser
|
* Class Parser
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Parser
|
* @package IndifferentKetchup\Codex\Parser
|
||||||
*/
|
*/
|
||||||
abstract class Parser implements ParserInterface
|
abstract class Parser implements ParserInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Aternos\Codex\Parser;
|
namespace IndifferentKetchup\Codex\Parser;
|
||||||
|
|
||||||
use Aternos\Codex\Log\LogInterface;
|
use IndifferentKetchup\Codex\Log\LogInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface ParserInterface
|
* Interface ParserInterface
|
||||||
*
|
*
|
||||||
* @package Aternos\Codex\Parser
|
* @package IndifferentKetchup\Codex\Parser
|
||||||
*/
|
*/
|
||||||
interface ParserInterface
|
interface ParserInterface
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user