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;
|
||||
|
||||
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\WeightedSinglePatternDetector;
|
||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||
@@ -22,7 +22,7 @@ class ProjectZomboidItemLog extends ProjectZomboidEventLog
|
||||
|
||||
public static function getDefaultAnalyser(): AnalyserInterface
|
||||
{
|
||||
return new PatternAnalyser();
|
||||
return new ItemDuplicationAnalyser();
|
||||
}
|
||||
|
||||
public static function getDetectors(): array
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||
|
||||
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\WeightedSinglePatternDetector;
|
||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||
@@ -22,7 +22,7 @@ class ProjectZomboidPerkLog extends ProjectZomboidEventLog
|
||||
|
||||
public static function getDefaultAnalyser(): AnalyserInterface
|
||||
{
|
||||
return new PatternAnalyser();
|
||||
return new SkillProgressionAnomalyAnalyser();
|
||||
}
|
||||
|
||||
public static function getDetectors(): array
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace IndifferentKetchup\Codex\Log\ProjectZomboid;
|
||||
|
||||
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\WeightedSinglePatternDetector;
|
||||
use IndifferentKetchup\Codex\Parser\ParserInterface;
|
||||
@@ -22,7 +22,7 @@ class ProjectZomboidUserLog extends ProjectZomboidEventLog
|
||||
|
||||
public static function getDefaultAnalyser(): AnalyserInterface
|
||||
{
|
||||
return new PatternAnalyser();
|
||||
return new ConnectionFailureAnalyser();
|
||||
}
|
||||
|
||||
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: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: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: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 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->parse();
|
||||
|
||||
$this->assertCount(10, $log->getEntries());
|
||||
$this->assertCount(20, $log->getEntries());
|
||||
}
|
||||
|
||||
public function testFieldsRegexExtractsItemAndDelta(): void
|
||||
|
||||
@@ -20,7 +20,7 @@ class ProjectZomboidPerkLogTest extends TestCase
|
||||
$log = (new ProjectZomboidPerkLog())->setLogFile(new PathLogFile($this->fixturePath()));
|
||||
$log->parse();
|
||||
|
||||
$this->assertCount(6, $log->getEntries());
|
||||
$this->assertCount(10, $log->getEntries());
|
||||
}
|
||||
|
||||
public function testFieldsRegexHandlesEventRow(): void
|
||||
|
||||
Reference in New Issue
Block a user