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))); } }