Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion bin/phpboy.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ function showHelp(): void
--playback=<path> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')) {
Expand Down
30 changes: 28 additions & 2 deletions src/Frontend/Cli/CliInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string, Button> Keyboard key to button mapping */
private const KEY_MAP = [
// Arrow keys (ANSI escape sequences)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down