Compare commits
4 Commits
c57d646229
...
0c90e40a28
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c90e40a28 | |||
| ba3fae8736 | |||
| 73e9ca6181 | |||
| c444e8543b |
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ItemDuplicationAnalyser;
|
||||||
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
||||||
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
||||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
@@ -22,7 +22,7 @@ class ProjectZomboidItemLog extends ProjectZomboidEventLog
|
|||||||
|
|
||||||
public static function getDefaultAnalyser(): AnalyserInterface
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
{
|
{
|
||||||
return new PatternAnalyser();
|
return new ItemDuplicationAnalyser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDetectors(): array
|
public static function getDetectors(): array
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\SkillProgressionAnomalyAnalyser;
|
||||||
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
||||||
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
||||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
@@ -22,7 +22,7 @@ class ProjectZomboidPerkLog extends ProjectZomboidEventLog
|
|||||||
|
|
||||||
public static function getDefaultAnalyser(): AnalyserInterface
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
{
|
{
|
||||||
return new PatternAnalyser();
|
return new SkillProgressionAnomalyAnalyser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDetectors(): array
|
public static function getDetectors(): array
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||||
|
|
||||||
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
use IndifferentKetchup\Codex\Analyser\AnalyserInterface;
|
||||||
use IndifferentKetchup\Codex\Analyser\PatternAnalyser;
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ConnectionFailureAnalyser;
|
||||||
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
use IndifferentKetchup\Codex\Detective\FilenameDetector;
|
||||||
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
use IndifferentKetchup\Codex\Detective\WeightedSinglePatternDetector;
|
||||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||||
@@ -22,7 +22,7 @@ class ProjectZomboidUserLog extends ProjectZomboidEventLog
|
|||||||
|
|
||||||
public static function getDefaultAnalyser(): AnalyserInterface
|
public static function getDefaultAnalyser(): AnalyserInterface
|
||||||
{
|
{
|
||||||
return new PatternAnalyser();
|
return new ConnectionFailureAnalyser();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getDetectors(): array
|
public static function getDetectors(): array
|
||||||
|
|||||||
@@ -8,3 +8,13 @@
|
|||||||
[16-04-26 19:35:42.812] 76561198000000001 "Player1" container -1 1002,2002,0 [Base.WaterBottleFull].
|
[16-04-26 19:35:42.812] 76561198000000001 "Player1" container -1 1002,2002,0 [Base.WaterBottleFull].
|
||||||
[16-04-26 19:40:00.514] 76561198000000002 "Player2" floor +1 1011,2011,0 [Base.Bandage].
|
[16-04-26 19:40:00.514] 76561198000000002 "Player2" floor +1 1011,2011,0 [Base.Bandage].
|
||||||
[16-04-26 19:42:25.223] 76561198000000002 "Player2" inventory +5 1011,2011,0 [Base.Bullets9mm].
|
[16-04-26 19:42:25.223] 76561198000000002 "Player2" inventory +5 1011,2011,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.001] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.002] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.003] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.004] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.005] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 19:50:00.006] 76561198000000003 "AdminUser" inventory +1 1020,2020,0 [Base.Bullets9mm].
|
||||||
|
[16-04-26 20:00:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank].
|
||||||
|
[16-04-26 20:01:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank].
|
||||||
|
[16-04-26 20:02:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank].
|
||||||
|
[16-04-26 20:03:00.000] 76561198000000001 "Player1" floor +1 1004,2004,0 [Base.Plank].
|
||||||
|
|||||||
@@ -4,3 +4,7 @@
|
|||||||
[16-04-26 18:29:15.823] [76561198000000002][Player2][1010,2010,0][Cooking=2, Fitness=10, Strength=10, Blunt=10, Axe=0, Lightfoot=4, Nimble=4, Sprinting=7, Sneak=2, Woodwork=6, Aiming=5, Reloading=4, Farming=0, Fishing=0, Trapping=0, PlantScavenging=0, Doctor=2, Electricity=5, Blacksmith=8, MetalWelding=10, Mechanics=10, Spear=0, Maintenance=7, SmallBlade=0, LongBlade=0, SmallBlunt=0, Tailoring=0, Tracking=0, Husbandry=0, FlintKnapping=0, Masonry=0, Pottery=0, Carving=0, Butchering=0, Glassmaking=0, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 50].
|
[16-04-26 18:29:15.823] [76561198000000002][Player2][1010,2010,0][Cooking=2, Fitness=10, Strength=10, Blunt=10, Axe=0, Lightfoot=4, Nimble=4, Sprinting=7, Sneak=2, Woodwork=6, Aiming=5, Reloading=4, Farming=0, Fishing=0, Trapping=0, PlantScavenging=0, Doctor=2, Electricity=5, Blacksmith=8, MetalWelding=10, Mechanics=10, Spear=0, Maintenance=7, SmallBlade=0, LongBlade=0, SmallBlunt=0, Tailoring=0, Tracking=0, Husbandry=0, FlintKnapping=0, Masonry=0, Pottery=0, Carving=0, Butchering=0, Glassmaking=0, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 50].
|
||||||
[16-04-26 18:30:02.500] [76561198000000003][AdminUser][1020,2020,0][Logout][Hours Survived: 75].
|
[16-04-26 18:30:02.500] [76561198000000003][AdminUser][1020,2020,0][Logout][Hours Survived: 75].
|
||||||
[16-04-26 19:15:00.000] [76561198000000001][Player1][1003,2003,1][LevelUp][Hours Survived: 101].
|
[16-04-26 19:15:00.000] [76561198000000001][Player1][1003,2003,1][LevelUp][Hours Survived: 101].
|
||||||
|
[16-04-26 18:30:00.000] [76561198000000004][PlayerSuspect][1030,2030,0][Login][Hours Survived: 10].
|
||||||
|
[16-04-26 18:30:00.000] [76561198000000004][PlayerSuspect][1030,2030,0][Cooking=2, Fitness=2, Strength=2, Blunt=0, Axe=0, Lightfoot=0, Nimble=0, Sprinting=0, Sneak=0, Woodwork=0, Aiming=0, Reloading=0, Farming=0, Fishing=0, Trapping=0, PlantScavenging=0, Doctor=0, Electricity=0, Blacksmith=0, MetalWelding=0, Mechanics=0, Spear=0, Maintenance=0, SmallBlade=0, LongBlade=0, SmallBlunt=0, Tailoring=0, Tracking=0, Husbandry=0, FlintKnapping=0, Masonry=0, Pottery=0, Carving=0, Butchering=0, Glassmaking=0, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 10].
|
||||||
|
[16-04-26 22:00:00.000] [76561198000000004][PlayerSuspect][1031,2031,0][Login][Hours Survived: 12].
|
||||||
|
[16-04-26 22:00:00.000] [76561198000000004][PlayerSuspect][1031,2031,0][Cooking=2, Fitness=8, Strength=10, Blunt=0, Axe=0, Lightfoot=0, Nimble=0, Sprinting=0, Sneak=0, Woodwork=0, Aiming=0, Reloading=0, Farming=0, Fishing=0, Trapping=0, PlantScavenging=0, Doctor=0, Electricity=0, Blacksmith=0, MetalWelding=0, Mechanics=0, Spear=0, Maintenance=3, SmallBlade=0, LongBlade=0, SmallBlunt=0, Tailoring=0, Tracking=0, Husbandry=0, FlintKnapping=0, Masonry=0, Pottery=0, Carving=0, Butchering=0, Glassmaking=0, Side_L=0, Side_R=0, ProstFamiliarity=0][Hours Survived: 12].
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\ItemDuplicationAnalyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ItemDuplicationProblem;
|
||||||
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidItemLog;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ItemLogAnalysisTest extends TestCase
|
||||||
|
{
|
||||||
|
private function fixturePath(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/item-minimal.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlagsBurstOfSameItemAboveThreshold(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ItemDuplicationProblem::class);
|
||||||
|
$this->assertCount(1, $problems);
|
||||||
|
|
||||||
|
$problem = $problems[0];
|
||||||
|
$this->assertSame('76561198000000003', $problem->getSteamId());
|
||||||
|
$this->assertSame('AdminUser', $problem->getPlayer());
|
||||||
|
$this->assertSame('Base.Bullets9mm', $problem->getItem());
|
||||||
|
$this->assertSame(6, $problem->getEventCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotFlagSubThresholdGroup(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ItemDuplicationProblem::class);
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertNotSame('Base.Plank', $problem->getItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThresholdConstantsAreDocumentedAndPositive(): void
|
||||||
|
{
|
||||||
|
$this->assertGreaterThan(0, ItemDuplicationAnalyser::THRESHOLD_COUNT);
|
||||||
|
$this->assertGreaterThan(0, ItemDuplicationAnalyser::THRESHOLD_WINDOW_SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analyser\ProjectZomboid\SkillProgressionAnomalyAnalyser;
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\SkillProgressionAnomalyProblem;
|
||||||
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidPerkLog;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class PerkLogAnalysisTest extends TestCase
|
||||||
|
{
|
||||||
|
private function fixturePath(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/perk-minimal.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlagsSkillsThatExceedDeltaThreshold(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(SkillProgressionAnomalyProblem::class);
|
||||||
|
|
||||||
|
$skills = array_map(fn($p) => $p->getSkill(), $problems);
|
||||||
|
sort($skills);
|
||||||
|
|
||||||
|
$this->assertSame(['Fitness', 'Strength'], $skills);
|
||||||
|
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertSame('76561198000000004', $problem->getSteamId());
|
||||||
|
$this->assertSame('PlayerSuspect', $problem->getPlayer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeltaAtThresholdDoesNotTrigger(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(SkillProgressionAnomalyProblem::class);
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertNotSame('Maintenance', $problem->getSkill());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSinglePlayerWithOneSnapshotProducesNoProblem(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(SkillProgressionAnomalyProblem::class);
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertNotSame('76561198000000001', $problem->getSteamId());
|
||||||
|
$this->assertNotSame('76561198000000002', $problem->getSteamId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testThresholdConstantIsDocumentedAndPositive(): void
|
||||||
|
{
|
||||||
|
$this->assertGreaterThan(0, SkillProgressionAnomalyAnalyser::THRESHOLD_DELTA);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace IndifferentKetchup\Codex\Test\Tests\Games\ProjectZomboid\Analyser;
|
||||||
|
|
||||||
|
use IndifferentKetchup\Codex\Analysis\ProjectZomboid\ConnectionFailureProblem;
|
||||||
|
use IndifferentKetchup\Codex\Log\File\PathLogFile;
|
||||||
|
use IndifferentKetchup\Codex\Log\ProjectZomboid\ProjectZomboidUserLog;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class UserLogAnalysisTest extends TestCase
|
||||||
|
{
|
||||||
|
private function fixturePath(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/../../../../src/Games/ProjectZomboid/fixtures/user-minimal.txt';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFlagsPlayerWithUnmatchedAttempts(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidUserLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ConnectionFailureProblem::class);
|
||||||
|
$this->assertCount(1, $problems);
|
||||||
|
|
||||||
|
$problem = $problems[0];
|
||||||
|
$this->assertSame('76561198000000001', $problem->getSteamId());
|
||||||
|
$this->assertSame('Player1', $problem->getPlayer());
|
||||||
|
$this->assertSame(1, $problem->getUnmatchedAttempts());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotFlagPlayerWithMatchedAttempts(): void
|
||||||
|
{
|
||||||
|
$log = (new ProjectZomboidUserLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
|
$log->parse();
|
||||||
|
$analysis = $log->analyse();
|
||||||
|
|
||||||
|
$problems = $analysis->getFilteredInsights(ConnectionFailureProblem::class);
|
||||||
|
foreach ($problems as $problem) {
|
||||||
|
$this->assertNotSame('76561198000000002', $problem->getSteamId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ class ProjectZomboidItemLogTest extends TestCase
|
|||||||
$log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
$log = (new ProjectZomboidItemLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
$log->parse();
|
$log->parse();
|
||||||
|
|
||||||
$this->assertCount(10, $log->getEntries());
|
$this->assertCount(20, $log->getEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFieldsRegexExtractsItemAndDelta(): void
|
public function testFieldsRegexExtractsItemAndDelta(): void
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ProjectZomboidPerkLogTest extends TestCase
|
|||||||
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||||
$log->parse();
|
$log->parse();
|
||||||
|
|
||||||
$this->assertCount(6, $log->getEntries());
|
$this->assertCount(10, $log->getEntries());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testFieldsRegexHandlesEventRow(): void
|
public function testFieldsRegexHandlesEventRow(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user