First complete working implementation with time of day and play time rules
This commit is contained in:
parent
049aa19dce
commit
7bc2e364a8
3
.gitignore
vendored
3
.gitignore
vendored
@ -23,3 +23,6 @@
|
|||||||
/public/assets/
|
/public/assets/
|
||||||
/assets/vendor/
|
/assets/vendor/
|
||||||
###< symfony/asset-mapper ###
|
###< symfony/asset-mapper ###
|
||||||
|
|
||||||
|
data.json
|
||||||
|
|
||||||
|
56
src/Command/CheckCommand.php
Normal file
56
src/Command/CheckCommand.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Service\Factorio;
|
||||||
|
use App\Service\RuleChecker;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:check',
|
||||||
|
description: 'Check all online players',
|
||||||
|
)]
|
||||||
|
class CheckCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly Factorio $factorio,
|
||||||
|
private readonly RuleChecker $ruleChecker,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$players = $this->factorio->getOnlinePlayers();
|
||||||
|
$rules = $this->ruleChecker->getRules();
|
||||||
|
foreach ($players as $player) {
|
||||||
|
$allowed = false;
|
||||||
|
foreach ($rules as $rule) {
|
||||||
|
$check = $rule->check($player);
|
||||||
|
// $this->factorio->whisper($player, sprintf('Checking %s, %s', $rule->getName(), $check));
|
||||||
|
if ($check->isWarning()) {
|
||||||
|
$this->factorio->whisper($player, sprintf('Checked %s. %s', $rule->getName(), $check));
|
||||||
|
}
|
||||||
|
if ($check->isAllowed()) {
|
||||||
|
$allowed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$allowed) {
|
||||||
|
$this->factorio->kickPlayer($player, 'Time has expired on this server for today');
|
||||||
|
$io->warning(sprintf('Kicked player %s', $player->getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
34
src/Command/TestCommand.php
Normal file
34
src/Command/TestCommand.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Service\Factorio;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:test',
|
||||||
|
description: 'Add a short description for your command',
|
||||||
|
)]
|
||||||
|
class TestCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(private readonly Factorio $factorio)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
$players = $this->factorio->getOnlinePlayers();
|
||||||
|
dump($players);
|
||||||
|
|
||||||
|
$this->factorio->kickPlayer("Ardentsword");
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
15
src/Dto/Player.php
Executable file
15
src/Dto/Player.php
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Dto;
|
||||||
|
|
||||||
|
class Player
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $name,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
66
src/Service/Factorio.php
Executable file
66
src/Service/Factorio.php
Executable file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Dto\Player;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
class Factorio
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $initPath = '/home/ardent/games/factorio-init/factorio',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function runBase(array|string $args): array
|
||||||
|
{
|
||||||
|
if (is_string($args)) {
|
||||||
|
$args = [$args];
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = new Process([$this->initPath, ...$args]);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
return explode(PHP_EOL, $process->getOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runCmd(array|string $args): array
|
||||||
|
{
|
||||||
|
if (is_string($args)) {
|
||||||
|
$args = [$args];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->runBase(['cmd', ...$args]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Player[]
|
||||||
|
*/
|
||||||
|
public function getOnlinePlayers(): array
|
||||||
|
{
|
||||||
|
$output = $this->runBase('players');
|
||||||
|
array_shift($output); // Remove header
|
||||||
|
array_pop($output); // Remove empty line
|
||||||
|
|
||||||
|
$players = [];
|
||||||
|
foreach ($output as $line) {
|
||||||
|
$players[] = new Player(explode(' ', trim($line))[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $players;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kickPlayer(Player $player, string $reason = ''): void
|
||||||
|
{
|
||||||
|
$this->runCmd(['/kick', $player->getName(), $reason]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function whisper(Player $player, string $message): void
|
||||||
|
{
|
||||||
|
$this->runCmd(['/whisper', $player->getName(), $message]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function say(string $message): void
|
||||||
|
{
|
||||||
|
$this->runCmd($message);
|
||||||
|
}
|
||||||
|
}
|
29
src/Service/RuleChecker.php
Executable file
29
src/Service/RuleChecker.php
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Dto\Player;
|
||||||
|
use App\Service\Rules\AbstractRule;
|
||||||
|
use App\Service\Rules\PlayTimeRule;
|
||||||
|
use App\Service\Rules\RuleResult;
|
||||||
|
use App\Service\Rules\TimeRule;
|
||||||
|
|
||||||
|
class RuleChecker
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TimeRule $timeRule,
|
||||||
|
private readonly PlayTimeRule $playTimeRule,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return AbstractRule[]
|
||||||
|
*/
|
||||||
|
public function getRules(): array
|
||||||
|
{
|
||||||
|
// Order is very important!
|
||||||
|
return [
|
||||||
|
$this->timeRule,
|
||||||
|
$this->playTimeRule,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
12
src/Service/Rules/AbstractRule.php
Executable file
12
src/Service/Rules/AbstractRule.php
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Rules;
|
||||||
|
|
||||||
|
use App\Dto\Player;
|
||||||
|
|
||||||
|
abstract class AbstractRule
|
||||||
|
{
|
||||||
|
abstract function check(Player $player): RuleResult;
|
||||||
|
|
||||||
|
abstract function getName();
|
||||||
|
}
|
73
src/Service/Rules/PlayTimeRule.php
Executable file
73
src/Service/Rules/PlayTimeRule.php
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Rules;
|
||||||
|
|
||||||
|
use App\Dto\Player;
|
||||||
|
use App\Service\SimpleDataService;
|
||||||
|
|
||||||
|
class PlayTimeRule extends AbstractRule
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly SimpleDataService $storeData,
|
||||||
|
private readonly int $defaultPlayTime = 60,
|
||||||
|
private readonly int $timeBetweenChecks = 1,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function timePerPlayer(Player $player)
|
||||||
|
{
|
||||||
|
$players = [
|
||||||
|
// "Ardentsword" => "2:00",
|
||||||
|
];
|
||||||
|
|
||||||
|
return $players[$player->getName()] ?? $this->defaultPlayTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function check(Player $player): RuleResult
|
||||||
|
{
|
||||||
|
$today = new \DateTimeImmutable();
|
||||||
|
$playerData = $this->getPlayerData($player);
|
||||||
|
|
||||||
|
// If the player has no data or the data is from a different day, we reset the time
|
||||||
|
if (!$playerData || $playerData['day'] !== $today->format('Y-m-d')) {
|
||||||
|
$this->storePlayerData($player, $today, $this->timePerPlayer($player));
|
||||||
|
return new RuleResult(RuleResultEnum::WARNING, sprintf('Extra time activated, set to %d minutes', $this->timePerPlayer($player)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the player has time left, we decrease it and return denied if nothing left
|
||||||
|
$timeLeft = $playerData['timeLeft'] - $this->timeBetweenChecks;
|
||||||
|
$this->storePlayerData($player, $today, $timeLeft);
|
||||||
|
|
||||||
|
if ($timeLeft <= 0) {
|
||||||
|
return new RuleResult(RuleResultEnum::DENIED);
|
||||||
|
} elseif ($timeLeft <= 5) {
|
||||||
|
return new RuleResult(RuleResultEnum::WARNING, sprintf('%d minute(s) left', $timeLeft));
|
||||||
|
}elseif($timeLeft === 30) {
|
||||||
|
return new RuleResult(RuleResultEnum::WARNING, sprintf('30 minutes left'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RuleResult(RuleResultEnum::ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return sprintf('PlayTimeRule, limited play time');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storePlayerData(Player $player, \DateTimeImmutable $day, int $timeLeft): void
|
||||||
|
{
|
||||||
|
$this->storeData->set(
|
||||||
|
sprintf('playTime-%s', $player->getName()),
|
||||||
|
[
|
||||||
|
'player' => $player->getName(),
|
||||||
|
'timeLeft' => $timeLeft,
|
||||||
|
'day' => $day->format('Y-m-d'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPlayerData(Player $player): ?array
|
||||||
|
{
|
||||||
|
return $this->storeData->get(
|
||||||
|
sprintf('playTime-%s', $player->getName())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
35
src/Service/Rules/RuleResult.php
Executable file
35
src/Service/Rules/RuleResult.php
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Rules;
|
||||||
|
|
||||||
|
class RuleResult
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RuleResultEnum $state,
|
||||||
|
private readonly string $message = '',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$message = $this->state->getString();
|
||||||
|
if ($this->message !== '') {
|
||||||
|
$message .= ': ' . $this->message;
|
||||||
|
}
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowed(): bool
|
||||||
|
{
|
||||||
|
return $this->state !== RuleResultEnum::DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isWarning(): bool
|
||||||
|
{
|
||||||
|
return $this->state === RuleResultEnum::WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMessage(): string
|
||||||
|
{
|
||||||
|
return $this->message;
|
||||||
|
}
|
||||||
|
}
|
19
src/Service/Rules/RuleResultEnum.php
Executable file
19
src/Service/Rules/RuleResultEnum.php
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Rules;
|
||||||
|
|
||||||
|
enum RuleResultEnum: int
|
||||||
|
{
|
||||||
|
case ALLOWED = 10;
|
||||||
|
case WARNING = 20;
|
||||||
|
case DENIED = 30;
|
||||||
|
|
||||||
|
public function getString(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::ALLOWED => 'Allowed',
|
||||||
|
self::WARNING => 'Warning',
|
||||||
|
self::DENIED => 'Denied',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
35
src/Service/Rules/TimeRule.php
Executable file
35
src/Service/Rules/TimeRule.php
Executable file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service\Rules;
|
||||||
|
|
||||||
|
use App\Dto\Player;
|
||||||
|
|
||||||
|
class TimeRule extends AbstractRule
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly string $start = '21:00',
|
||||||
|
private readonly string $end = '23:59',
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function check(Player $player): RuleResult
|
||||||
|
{
|
||||||
|
$now = new \DateTimeImmutable();
|
||||||
|
$start = new \DateTimeImmutable($this->start);
|
||||||
|
$end = new \DateTimeImmutable($this->end);
|
||||||
|
$fiveMinutesBeforeEnd = $end->sub(new \DateInterval('PT5M'));
|
||||||
|
|
||||||
|
if ($now >= $start && $now <= $end) {
|
||||||
|
if ($now >= $fiveMinutesBeforeEnd) {
|
||||||
|
return new RuleResult(RuleResultEnum::WARNING, 'Less than 5 minutes remaining');
|
||||||
|
}
|
||||||
|
return new RuleResult(RuleResultEnum::ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RuleResult(RuleResultEnum::DENIED, 'After designed hours');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return sprintf('TimeRule, time is between %s and %s', $this->start, $this->end);
|
||||||
|
}
|
||||||
|
}
|
38
src/Service/SimpleDataService.php
Executable file
38
src/Service/SimpleDataService.php
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\KernelInterface;
|
||||||
|
|
||||||
|
class SimpleDataService
|
||||||
|
{
|
||||||
|
private string $file;
|
||||||
|
|
||||||
|
public function __construct(KernelInterface $kernel)
|
||||||
|
{
|
||||||
|
$this->file = $kernel->getProjectDir() . '/data.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, array $data): self
|
||||||
|
{
|
||||||
|
$all = $this->getAll();
|
||||||
|
$all[$key] = $data;
|
||||||
|
|
||||||
|
file_put_contents($this->file, json_encode($all, JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): ?array
|
||||||
|
{
|
||||||
|
return $this->getAll()[$key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll(): array
|
||||||
|
{
|
||||||
|
if (!file_exists($this->file)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return json_decode(file_get_contents($this->file), true);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user