initial commit
This commit is contained in:
commit
e8c3f9862a
193 files changed
+29795
No files matched your search
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Game\GameSimulator;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
/**
|
||||
* Long-running server-authoritative simulation loop.
|
||||
*
|
||||
* Reads control + input frames pushed to Redis (by the HTTP/reverb process),
|
||||
* applies them to the world owned by GameSimulator, advances the sim at a
|
||||
* fixed 60 Hz step, and broadcasts 30 Hz snapshots to the private game.{room}
|
||||
* channel through the configured broadcaster.
|
||||
*/
|
||||
final class GameSimulate extends Command
|
||||
{
|
||||
protected $signature = 'game:simulate';
|
||||
|
||||
protected $description = 'Run the authoritative game simulation worker';
|
||||
|
||||
private const TICK_DT = 1.0 / 60.0;
|
||||
|
||||
private const MAX_TICKS_PER_LOOP = 8;
|
||||
|
||||
private float $accumulator = 0.0;
|
||||
|
||||
private bool $running = true;
|
||||
|
||||
private int $loop = 0;
|
||||
|
||||
public function handle(GameSimulator $simulator): int
|
||||
{
|
||||
$this->registerSignals();
|
||||
|
||||
$last = hrtime(true) / 1e9;
|
||||
while ($this->running) {
|
||||
$now = hrtime(true) / 1e9;
|
||||
$delta = min(0.25, max(0.0, $now - $last));
|
||||
$last = $now;
|
||||
$this->accumulator += $delta;
|
||||
|
||||
$ticks = 0;
|
||||
while ($this->accumulator >= self::TICK_DT && $ticks < self::MAX_TICKS_PER_LOOP) {
|
||||
$this->drainQueues($simulator);
|
||||
$simulator->tick(self::TICK_DT);
|
||||
$this->accumulator -= self::TICK_DT;
|
||||
$ticks++;
|
||||
}
|
||||
|
||||
if (++$this->loop % 600 === 0) {
|
||||
$this->heartbeat($simulator);
|
||||
}
|
||||
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
$this->info('Simulation worker stopped.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function registerSignals(): void
|
||||
{
|
||||
if (!function_exists('pcntl_signal')) {
|
||||
$this->warn('pcntl unavailable; graceful shutdown disabled.');
|
||||
|
||||
return;
|
||||
}
|
||||
pcntl_async_signals(true);
|
||||
pcntl_signal(SIGINT, function (): void {
|
||||
$this->running = false;
|
||||
});
|
||||
pcntl_signal(SIGTERM, function (): void {
|
||||
$this->running = false;
|
||||
});
|
||||
}
|
||||
|
||||
private function drainQueues(GameSimulator $simulator): void
|
||||
{
|
||||
$roomIds = $simulator->roomIds();
|
||||
foreach (Redis::keys(sprintf(GameSimulator::CONTROL_QUEUE, '*')) as $key) {
|
||||
if (preg_match('#^game:ctrl:(.+)$#', (string) $key, $matches) === 1) {
|
||||
$roomIds[] = $matches[1];
|
||||
}
|
||||
}
|
||||
$roomIds = array_values(array_unique($roomIds));
|
||||
|
||||
foreach ($roomIds as $roomId) {
|
||||
while (($control = Redis::lpop(sprintf(GameSimulator::CONTROL_QUEUE, $roomId))) !== false && $control !== null) {
|
||||
$frame = $simulator->decode($control);
|
||||
if ($frame !== null) {
|
||||
if ($this->getOutput()->isVerbose()) {
|
||||
$this->line(sprintf(' ctrl %s <- %s', $roomId, (string) ($frame['type'] ?? '?')));
|
||||
}
|
||||
$simulator->applyControl($roomId, $frame);
|
||||
}
|
||||
}
|
||||
if (!in_array($roomId, $simulator->roomIds(), true)) {
|
||||
continue;
|
||||
}
|
||||
while (($input = Redis::lpop(sprintf(GameSimulator::INPUT_QUEUE, $roomId))) !== false && $input !== null) {
|
||||
$frame = $simulator->decode($input);
|
||||
if ($frame !== null) {
|
||||
if ($this->getOutput()->isVerbose()) {
|
||||
$this->line(sprintf(' in %s <- %s#%s', $roomId, (string) ($frame['id'] ?? '?'), implode('+', (array) ($frame['keys'] ?? []))));
|
||||
}
|
||||
$simulator->applyInput($roomId, $frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function heartbeat(GameSimulator $simulator): void
|
||||
{
|
||||
if (!$this->getOutput()->isVerbose()) {
|
||||
return;
|
||||
}
|
||||
$rooms = [];
|
||||
foreach ($simulator->roomIds() as $roomId) {
|
||||
$room = $simulator->room($roomId);
|
||||
$rooms[] = sprintf('%s:%s(%d,%d)', $roomId, $room?->state ?? '?', count($room?->players ?? []), count($room?->entities ?? []));
|
||||
}
|
||||
$keys = Redis::keys('game:*') ?: [];
|
||||
$this->line(sprintf('hb frame=%d loop=%d rooms=[%s] queueKeys=%d', $simulator->frame(), $this->loop, implode(',', $rooms), count($keys)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user