diff --git a/bin/phpboy.php b/bin/phpboy.php index c35d151..1c4f299 100644 --- a/bin/phpboy.php +++ b/bin/phpboy.php @@ -70,6 +70,15 @@ function showHelp(): void --playback= Playback TAS input from JSON file --help Show this help message +Controls (during gameplay): + Arrow Keys / WASD: D-pad + Z: A button + X: B button + Enter: Start + Space: Select + Ctrl+S: Save state (creates timestamped save file) + Ctrl+C: Exit + Examples: php bin/phpboy.php tetris.gb php bin/phpboy.php --rom=tetris.gb --speed=2.0 @@ -312,6 +321,22 @@ function parseArguments(array $argv): array if (!$options['headless']) { $input = new CliInput(); $emulator->setInput($input); + + // Set up Ctrl+S save callback + $saveCounter = 0; + $romBaseName = pathinfo($options['rom'], PATHINFO_FILENAME); + $input->onSave(function () use ($emulator, &$saveCounter, $romBaseName) { + $saveCounter++; + $timestamp = date('Y-m-d_H-i-s'); + $filename = "{$romBaseName}_save_{$saveCounter}_{$timestamp}.state"; + + try { + $emulator->saveState($filename); + echo "\n[Saved state to: {$filename}]\n"; + } catch (\Throwable $e) { + echo "\n[Error saving state: {$e->getMessage()}]\n"; + } + }); } // Set up renderer @@ -508,7 +533,9 @@ function parseArguments(array $argv): array echo " Z: A button\n"; echo " X: B button\n"; echo " Enter: Start\n"; - echo " Space: Select\n\n"; + echo " Space: Select\n"; + echo " Ctrl+S: Save state\n"; + echo " Ctrl+C: Exit\n\n"; // Set up signal handler for graceful shutdown if (function_exists('pcntl_signal')) { diff --git a/src/Frontend/Cli/CliInput.php b/src/Frontend/Cli/CliInput.php index ccd097b..039c8a0 100644 --- a/src/Frontend/Cli/CliInput.php +++ b/src/Frontend/Cli/CliInput.php @@ -11,11 +11,13 @@ * CLI keyboard input handler for PHPBoy. * * Maps keyboard keys to Game Boy buttons: - * - Arrow keys → D-pad (Up, Down, Left, Right) + * - Arrow keys / WASD → D-pad (Up, Down, Left, Right) * - Z → A button * - X → B button * - Enter → Start - * - Right Shift → Select + * - Space → Select + * - Ctrl+S → Save state (via callback) + * - Ctrl+C → Exit emulation * * Note: Non-blocking keyboard input in PHP CLI is limited. * This implementation uses stream_select for non-blocking reads @@ -29,6 +31,9 @@ final class CliInput implements InputInterface /** Control character for Ctrl+C (ASCII 3) */ private const CTRL_C = "\x03"; + /** Control character for Ctrl+S (ASCII 19) */ + private const CTRL_S = "\x13"; + /** @var array Keyboard key to button mapping */ private const KEY_MAP = [ // Arrow keys (ANSI escape sequences) @@ -73,12 +78,25 @@ final class CliInput implements InputInterface /** @var bool Whether terminal mode has been set */ private bool $terminalModeSet = false; + /** @var callable|null Callback to invoke when Ctrl+S is pressed */ + private $onSaveCallback = null; + public function __construct() { $this->stdin = STDIN; $this->setupTerminal(); } + /** + * Set callback to invoke when Ctrl+S is pressed. + * + * @param callable $callback Function to call when user presses Ctrl+S for save + */ + public function onSave(callable $callback): void + { + $this->onSaveCallback = $callback; + } + public function __destruct() { $this->restoreTerminal(); @@ -201,6 +219,14 @@ private function parseInput(string $input): void exit(0); } + // Check for Ctrl+S (in raw mode, this comes through as ASCII 19) + if (str_contains($input, self::CTRL_S)) { + if ($this->onSaveCallback !== null) { + ($this->onSaveCallback)(); + } + // Don't return here - continue processing other input in the buffer + } + // Check for arrow key escape sequences (3 characters) if (strlen($input) >= 3 && $input[0] === "\033" && $input[1] === '[') { $sequence = substr($input, 0, 3);