The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
520 lines
24 KiB
C
520 lines
24 KiB
C
// voyager.c
|
|
|
|
// The Segan Voyager
|
|
// A SplitBit with a screen and a speaker attached
|
|
// Written by Anachronaut
|
|
//
|
|
// ---- What this is ----
|
|
//
|
|
// The same machine SplitBit runs, presented through a window instead of a terminal. Every
|
|
// instruction, every device and every cycle is in machine.c and shared; this file opens a
|
|
// window, gives the machine a slice of time per frame, and shows what came out.
|
|
//
|
|
// THAT ORDER MATTERS AND IS THE WHOLE DESIGN. The devices belong to the machine and advance
|
|
// on emulated cycles, so the same program produces the same frames and the same samples
|
|
// whether or not anybody is looking. Raylib presents; it does not decide. Which is what
|
|
// lets a test suite with no display hold this binary to the same behaviour as the other
|
|
// one.
|
|
//
|
|
// The window shows what the video device produced and decides nothing about it. Render is a
|
|
// pure function of video memory, so the same program draws the same picture whether or not
|
|
// anybody is watching - which is what lets a suite with no display check a screen.
|
|
|
|
#include "machine.h"
|
|
#include "video.h"
|
|
#include "pad.h"
|
|
#include "sound.h"
|
|
#include "io.h"
|
|
#include "utility.h"
|
|
#include "raylib.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <getopt.h>
|
|
|
|
// The window opens at the largest screen the device can produce, doubled, because a 640 by
|
|
// 400 window is small on a modern display and a 320 by 200 one is a postage stamp.
|
|
#define SCREEN_SCALE 2
|
|
|
|
// ---- Running without a window ----
|
|
//
|
|
// Taken out of the arguments here rather than in the shared parser, because it is a fact
|
|
// about this front end and the shared parser should not learn about a window that only one
|
|
// binary has. Everything else on the command line means exactly what it means to SplitBit.
|
|
//
|
|
// It exists so the suite can run this binary at all: a test machine has no display, and a
|
|
// front end that could only be exercised by a person looking at it would be a front end
|
|
// nothing checks. Headless, Voyager must print byte for byte what SplitBit prints, and
|
|
// Tests/voyager.sh holds it to that.
|
|
static int takeHeadless(int *argc, char *argv[]) {
|
|
int headless = 0;
|
|
int out = 0;
|
|
for (int i = 0; i < *argc; i++) {
|
|
if (strcmp(argv[i], "--headless") == 0) {
|
|
headless = 1;
|
|
continue;
|
|
}
|
|
argv[out++] = argv[i];
|
|
}
|
|
argv[out] = NULL;
|
|
*argc = out;
|
|
return headless;
|
|
}
|
|
|
|
// ---- The window, kept in one place ----
|
|
//
|
|
// Both the frame loop and the input hook have to be able to present, because a machine
|
|
// waiting for a key is still a machine somebody is looking at. A window that froze while a
|
|
// program asked a question would look broken every time it asked one.
|
|
static Texture2D screenTexture;
|
|
static int windowOpen = 0;
|
|
|
|
// ---- Keys are kept until they are asked for ----
|
|
//
|
|
// RAYLIB CLEARS ITS CHARACTER QUEUE ON EVERY POLL, and a poll happens inside EndDrawing, so
|
|
// a key survives exactly one frame unless something takes it. That is fine for a game that
|
|
// reads input every frame and wrong for everything else: Snake looks about ten times a
|
|
// second, so five keys in six were being thrown away by the next present before it ever
|
|
// glanced at them. The shell worked the whole time, because a blocking read presents and
|
|
// then looks immediately.
|
|
//
|
|
// So the window keeps its own queue, drained from Raylib at every present and emptied only
|
|
// when the console actually takes a byte. That is what the machine already promises - Snake's
|
|
// own comment says "the console keeps the next key until it is asked for" - and it makes the
|
|
// console's timing nobody else's business.
|
|
#define KEY_QUEUE 64
|
|
static unsigned char keyQueue[KEY_QUEUE];
|
|
static int keyHead = 0;
|
|
static int keyTail = 0;
|
|
|
|
static void keyPush(unsigned char byte) {
|
|
const int next = (keyTail + 1) % KEY_QUEUE;
|
|
if (next == keyHead) {
|
|
// Full, so the oldest goes. Somebody leaning on the keyboard while a program ignores
|
|
// it should not be able to push out what they typed most recently.
|
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
|
}
|
|
keyQueue[keyTail] = byte;
|
|
keyTail = next;
|
|
}
|
|
|
|
static int keyTake(void) {
|
|
if (keyHead == keyTail) {
|
|
return CONSOLE_NOTHING_YET;
|
|
}
|
|
const int byte = keyQueue[keyHead];
|
|
keyHead = (keyHead + 1) % KEY_QUEUE;
|
|
return byte;
|
|
}
|
|
|
|
// Everything Raylib has, taken before it can throw any of it away.
|
|
// ---- The pads, read as levels ----
|
|
//
|
|
// Once a frame, from whatever is actually there. THIS IS THE ONE THING THE CONSOLE CANNOT DO:
|
|
// a window knows which keys are down, a terminal only ever learns which one was pressed, and
|
|
// asking the console to report a key coming up would have been a promise it could keep behind
|
|
// a window and nowhere else.
|
|
//
|
|
// The keyboard drives pad nought as well as any real controller, so a game written for a pad
|
|
// is playable on a machine with none - and so is a game written for four, badly. What a game
|
|
// reads is the pad; it never learns which one of them somebody used.
|
|
static void readPads(void) {
|
|
for (int n = 0; n < PAD_COUNT; n++) {
|
|
uint8_t held = 0;
|
|
if (IsGamepadAvailable(n)) {
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) { held |= PAD_RIGHT; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) { held |= PAD_LEFT; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) { held |= PAD_DOWN; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_LEFT_FACE_UP)) { held |= PAD_UP; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_DOWN)) { held |= PAD_A; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)){ held |= PAD_B; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { held |= PAD_START; }
|
|
if (IsGamepadButtonDown(n, GAMEPAD_BUTTON_MIDDLE_LEFT)) { held |= PAD_SELECT; }
|
|
|
|
// ---- And the stick, which is what most people actually push ----
|
|
//
|
|
// The four buttons above are the D-PAD, and a lot of controllers made this
|
|
// century have one that nobody uses: the thumb goes on the stick, which reports
|
|
// as an axis and not as a button, so a pad that was plugged in and working did
|
|
// nothing at all.
|
|
//
|
|
// Past halfway counts as held. That is a blunt line and the right kind of blunt
|
|
// for a device that reports what is DOWN - a machine with eight bits a pad has
|
|
// nothing to say about three-fifths of a push, and picking the threshold here
|
|
// rather than in every program is the point of the pad being a device.
|
|
const float across = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_X);
|
|
const float down = GetGamepadAxisMovement(n, GAMEPAD_AXIS_LEFT_Y);
|
|
if (across > 0.5f) { held |= PAD_RIGHT; }
|
|
if (across < -0.5f) { held |= PAD_LEFT; }
|
|
if (down > 0.5f) { held |= PAD_DOWN; }
|
|
if (down < -0.5f) { held |= PAD_UP; }
|
|
}
|
|
if (n == 0) {
|
|
// ---- And the keyboard, on top of it ----
|
|
//
|
|
// Arrows or WASD for the direction, Z and X for the buttons. OR-ed with whatever
|
|
// a real pad is doing rather than chosen between, so unplugging one mid-game does
|
|
// not leave somebody holding nothing.
|
|
if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) { held |= PAD_RIGHT; }
|
|
if (IsKeyDown(KEY_LEFT) || IsKeyDown(KEY_A)) { held |= PAD_LEFT; }
|
|
if (IsKeyDown(KEY_DOWN) || IsKeyDown(KEY_S)) { held |= PAD_DOWN; }
|
|
if (IsKeyDown(KEY_UP) || IsKeyDown(KEY_W)) { held |= PAD_UP; }
|
|
if (IsKeyDown(KEY_Z)) { held |= PAD_A; }
|
|
if (IsKeyDown(KEY_X)) { held |= PAD_B; }
|
|
if (IsKeyDown(KEY_ENTER)) { held |= PAD_START; }
|
|
if (IsKeyDown(KEY_TAB)) { held |= PAD_SELECT; }
|
|
}
|
|
padSet(n, held);
|
|
}
|
|
}
|
|
|
|
static void drainKeyboard(void) {
|
|
int character;
|
|
while ((character = GetCharPressed()) > 0) {
|
|
if (character < 128) {
|
|
keyPush((unsigned char)character);
|
|
}
|
|
}
|
|
int key;
|
|
while ((key = GetKeyPressed()) > 0) {
|
|
// Only the keys a character queue does not carry, because they are not characters.
|
|
// Everything else has already arrived above, and taking it again would double it.
|
|
switch (key) {
|
|
case KEY_ENTER: case KEY_KP_ENTER: keyPush('\n'); break;
|
|
case KEY_BACKSPACE: keyPush(0x08); break;
|
|
case KEY_TAB: keyPush('\t'); break;
|
|
case KEY_ESCAPE: keyPush(0x1B); break;
|
|
// ---- And the keys that are not characters at all ----
|
|
//
|
|
// These used to fall through the default below and vanish, because there was no
|
|
// byte to turn them into. There is now, and it is the console's rather than
|
|
// this window's - a terminal reaches the same values by a different road, and a
|
|
// program is entitled not to know which of the two it is talking to.
|
|
case KEY_UP: keyPush(CONSOLE_KEY_UP); break;
|
|
case KEY_DOWN: keyPush(CONSOLE_KEY_DOWN); break;
|
|
case KEY_LEFT: keyPush(CONSOLE_KEY_LEFT); break;
|
|
case KEY_RIGHT: keyPush(CONSOLE_KEY_RIGHT); break;
|
|
case KEY_HOME: keyPush(CONSOLE_KEY_HOME); break;
|
|
case KEY_END: keyPush(CONSOLE_KEY_END); break;
|
|
case KEY_DELETE: keyPush(CONSOLE_KEY_DELETE); break;
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- The reset button ----
|
|
//
|
|
// EMULATOR MAGIC, AND KNOWN TO BE. There is no reset line on this machine yet and no keyboard
|
|
// controller to assert one: the window reaches in and pokes the same flag a program pokes
|
|
// through the machine port. When those are designed, a keyboard controller will have to see
|
|
// this gesture and pull reset REGARDLESS OF WHAT THE CPU IS DOING - which is the property
|
|
// that matters and the one a port write cannot have, since a port write needs a program
|
|
// willing and able to make it.
|
|
//
|
|
// The shape of that is already visible here. A reset is normally noticed between
|
|
// instructions, and a halted machine runs none - so the window asks every frame rather than
|
|
// leaving it to the machine to notice, which is what real hardware would do with a line.
|
|
//
|
|
// ON REAL HARDWARE THIS IS NOT A KEY AT ALL. A Voyager has a button on the case, and what a
|
|
// window has instead of a case is a gesture. So the gesture wants two properties a single
|
|
// key does not have.
|
|
//
|
|
// It must not be a key SOFTWARE MIGHT WANT. A machine with a keyboard has function keys on
|
|
// it, and something will eventually have a use for F12 - which is where this was, and which
|
|
// would have meant taking it away again later.
|
|
//
|
|
// And it must not be reachable BY ACCIDENT. Restarting the machine throws away everything in
|
|
// memory, and a single key that does that sits one mistake away from losing work. Three keys
|
|
// together are not pressed by mistake.
|
|
//
|
|
// Control, Shift and R. It was Control, Alt and Delete, which has meant this since 1981 and
|
|
// is the one gesture nobody has to be told the meaning of - AND WHICH CANNOT BE USED.
|
|
//
|
|
// It is a secure attention key. Every serious operating system reserves it so that it always
|
|
// reaches the system and never an application, precisely so that a program cannot imitate a
|
|
// login screen; on Windows an application cannot see it at all without a kernel driver, and
|
|
// on Linux the desktop takes it. That is not an oversight to work around - it is the same
|
|
// guarantee a reset button wants, being enforced one layer further down, and there is no
|
|
// call this program can make that would win the argument.
|
|
//
|
|
// So the gesture has to be one the host has no opinion about. Control and Shift with a
|
|
// letter is about as free as a combination gets: it is not window management, not a virtual
|
|
// terminal switch, and not a shortcut any desktop claims by default.
|
|
//
|
|
// If a platform does send a character for it, nothing comes of that either - whatever
|
|
// arrives is in memory that is about to be thrown away.
|
|
//
|
|
// What it does is what writing MACHINE_RESET does: the machine starts the way it started, so
|
|
// the boot chain runs again and finds whatever the disk now says to run. Which is what makes
|
|
// a bare metal program escapable - Once puts a demo in front of the next start and deletes
|
|
// the request before jumping, so a demo that has taken the whole machine is one gesture from
|
|
// the system coming back, rather than closing the window and opening it again.
|
|
static void checkResetButton(void) {
|
|
const int control = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
|
|
const int shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
|
|
if (control && shift && IsKeyPressed(KEY_R)) {
|
|
requestReset();
|
|
}
|
|
}
|
|
|
|
// ---- The speaker ----
|
|
//
|
|
// The device makes its samples on emulated cycles and puts them in a ring; this takes them
|
|
// out and hands them to Raylib. Nothing here decides what a sound is, the same as nothing in
|
|
// presentFrame decides what the screen looks like - which is why the headless binary and this
|
|
// one make the same sound, and why Tests/sound.sh can check a device with no speaker in it.
|
|
//
|
|
// A sub-buffer at a time, because that is the unit Raylib hands back when it has finished
|
|
// playing one. At 48,000 a second and sixty frames, a frame of machine time is 800 samples,
|
|
// so 1,024 is a little over one and there are two of them.
|
|
#define SOUND_BUFFER 1024
|
|
static AudioStream soundStream;
|
|
static int speakerOn;
|
|
|
|
// ---- When the machine cannot keep up, and when it runs away ----
|
|
//
|
|
// Both directions happen and neither should be a crash. The machine runs a slice per frame
|
|
// against the wall clock, so a host that stalls leaves the ring short and a host running
|
|
// --fast fills it faster than anything can play it. The device drops when full, which is the
|
|
// runaway case. This is the other one: what is missing is filled by HOLDING THE LAST SAMPLE
|
|
// rather than by zeroes, because a jump to silence and back is a click and a held level is
|
|
// not. It is still a glitch; it is the quieter kind.
|
|
static void feedSpeaker(void) {
|
|
static int16_t buffer[SOUND_BUFFER];
|
|
static int16_t lastSample;
|
|
while (IsAudioStreamProcessed(soundStream)) {
|
|
const int taken = soundTake(buffer, SOUND_BUFFER);
|
|
if (taken > 0) {
|
|
lastSample = buffer[taken - 1];
|
|
}
|
|
for (int i = taken; i < SOUND_BUFFER; i++) {
|
|
buffer[i] = lastSample;
|
|
}
|
|
UpdateAudioStream(soundStream, buffer, SOUND_BUFFER);
|
|
}
|
|
}
|
|
|
|
static void presentFrame(void) {
|
|
// The device turns video memory into pixels; this puts them on the glass. Everything
|
|
// that decides what the screen looks like is in the machine, where the suite can
|
|
// reach it.
|
|
videoRender();
|
|
int width, height;
|
|
const uint8_t *frame = videoPixels(&width, &height);
|
|
if (width > 0 && height > 0) {
|
|
UpdateTextureRec(screenTexture, (Rectangle){ 0, 0, (float)width, (float)height },
|
|
frame);
|
|
}
|
|
BeginDrawing();
|
|
// Clearly not the screen. What is left over when the window's shape does not match the
|
|
// picture's is a bezel, and it should look like one rather than like more screen.
|
|
ClearBackground((Color){ 40, 40, 40, 255 });
|
|
if (width > 0 && height > 0) {
|
|
// ---- Filling the window, in whole pixels ----
|
|
//
|
|
// The largest whole-number scale that still fits. Whole numbers because a 320 by 200
|
|
// picture stretched by 2.7 is a picture with some rows twice as tall as their
|
|
// neighbours, which on eight pixel glyphs is the difference between text and mush.
|
|
//
|
|
// The two modes are exactly a factor of two apart and the window opens at twice the
|
|
// larger, so both fill it exactly: 320 by 200 at four, and 640 by 400 at two.
|
|
// Changing mode therefore changes how sharp the screen is and not how big it is.
|
|
const int windowWidth = GetScreenWidth();
|
|
const int windowHeight = GetScreenHeight();
|
|
int scale = windowWidth / width;
|
|
const int fits = windowHeight / height;
|
|
if (fits < scale) scale = fits;
|
|
if (scale < 1) scale = 1;
|
|
const int drawnWidth = width * scale;
|
|
const int drawnHeight = height * scale;
|
|
Rectangle from = { 0, 0, (float)width, (float)height };
|
|
Rectangle to = {
|
|
(float)((windowWidth - drawnWidth) / 2),
|
|
(float)((windowHeight - drawnHeight) / 2),
|
|
(float)drawnWidth, (float)drawnHeight
|
|
};
|
|
DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
|
}
|
|
EndDrawing();
|
|
// EndDrawing has just polled, which is the one moment Raylib's queues hold anything.
|
|
drainKeyboard();
|
|
readPads();
|
|
checkResetButton();
|
|
}
|
|
|
|
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
|
// window answering, and EndDrawing paces it, so waiting for a key costs a frame rather
|
|
// than a spin.
|
|
static int voyagerKey(int mayWait) {
|
|
if (!windowOpen) {
|
|
return CONSOLE_GONE;
|
|
}
|
|
// ---- The button has to reach a machine that is waiting ----
|
|
//
|
|
// A reset is acted on between instructions, and a machine blocked on a key is part way
|
|
// through one - so pressing the button while a program sits waiting would set the flag
|
|
// and nothing would ever come along to notice it. Which is precisely the moment a reset
|
|
// button earns its keep: a program that is stuck is the one you want to get out of.
|
|
//
|
|
// So the wait ends. The console treats that as the end of input, which it is for the
|
|
// machine that is about to stop existing, and the reset puts the console's input back.
|
|
//
|
|
// ASKED OF THE REQUEST ITSELF rather than remembered here. A flag of its own outlived
|
|
// the reset it belonged to: a program that never read the console - picture.bin, say,
|
|
// which draws and halts - left it set, and the NEXT machine's first read came back as
|
|
// the end of input. CosmOS booted and stopped immediately, having been told there was
|
|
// nobody there. There is one fact and it lives in one place.
|
|
if (resetIsPending()) {
|
|
return CONSOLE_GONE;
|
|
}
|
|
// Whatever is already waiting, however long ago it was typed. This is the answer to
|
|
// both questions, and asking it first is what makes a program that polls rarely see
|
|
// every key rather than one in six.
|
|
const int waiting = keyTake();
|
|
if (waiting != CONSOLE_NOTHING_YET) {
|
|
return waiting;
|
|
}
|
|
if (!mayWait) {
|
|
// A poll is a poll. Presenting here would charge a frame for every glance, and a
|
|
// program that looks in a loop would run at the frame rate.
|
|
return CONSOLE_NOTHING_YET;
|
|
}
|
|
if (WindowShouldClose()) {
|
|
windowOpen = 0;
|
|
return CONSOLE_GONE;
|
|
}
|
|
// Presenting is what keeps the window answering while the machine waits, and EndDrawing
|
|
// paces it, so waiting for a key costs a frame rather than a spin. It drains the
|
|
// keyboard on the way out, so anything just typed is here now.
|
|
presentFrame();
|
|
return keyTake();
|
|
}
|
|
|
|
|
|
|
|
int main(int argc, char *argv[]) {
|
|
int headless = takeHeadless(&argc, argv);
|
|
|
|
EmulatorOptions options;
|
|
uint8_t result = parseOptions(argc, argv, &options);
|
|
if (result == OPTIONS_HELP) {
|
|
printf(" --headless Run with no window, which is how the tests run it.\n");
|
|
return 0;
|
|
} else if (result == OPTIONS_ERROR) {
|
|
return 1;
|
|
}
|
|
char *programFile = NULL;
|
|
if (optind < argc) {
|
|
programFile = argv[optind];
|
|
optind++;
|
|
}
|
|
if (optind < argc) {
|
|
fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]);
|
|
return 1;
|
|
}
|
|
|
|
Machine machine;
|
|
uint8_t started = machineStart(&machine, &options, programFile);
|
|
if (started == MACHINE_NOTHING_TO_RUN) {
|
|
fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n");
|
|
printHelp(argv[0]);
|
|
return 1;
|
|
} else if (started != MACHINE_OK) {
|
|
return 1;
|
|
}
|
|
|
|
if (headless) {
|
|
// The same three lines SplitBit runs, and deliberately so: a headless Voyager is
|
|
// not a reduced machine, it is the machine with nobody watching.
|
|
while (machineRunning(&machine)) {
|
|
machineRunSlice(&machine);
|
|
}
|
|
} else {
|
|
// Resizable, because how big somebody wants a screen is not the machine's business.
|
|
// The picture is rescaled to whatever the window becomes, in whole pixels.
|
|
//
|
|
// And presented in step with the display. Without the hint the frame limiter sleeps
|
|
// towards sixty a second on its own clock, which beats against a screen refreshing on
|
|
// its own - some frames shown twice, some skipped, and the machine handed an uneven
|
|
// number of cycles each time because it takes them from the wall clock. The target
|
|
// stays as well, for a driver that ignores the hint.
|
|
SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT);
|
|
InitWindow(VIDEO_MAX_WIDTH * SCREEN_SCALE, VIDEO_MAX_HEIGHT * SCREEN_SCALE,
|
|
"Segan Voyager");
|
|
SetTargetFPS(60);
|
|
// ---- Escape is a byte, not a way out ----
|
|
//
|
|
// Raylib closes a window on Escape unless it is told not to, and this machine sends
|
|
// Escape to the console like any other key. So a program reading keys could be
|
|
// ended by one of them, taking whatever was in memory with it - which is a poor way
|
|
// to find out that a default was left as it was found.
|
|
SetExitKey(KEY_NULL);
|
|
|
|
// ---- What controllers the host can see, said out loud ----
|
|
//
|
|
// A pad that is plugged in and does nothing is indistinguishable from a pad the front
|
|
// end never noticed, and the difference is the whole of what to do about it: one is a
|
|
// mapping to fix and the other is a driver. Saying which at startup costs a line and
|
|
// answers it without anybody having to guess.
|
|
for (int n = 0; n < PAD_COUNT; n++) {
|
|
if (IsGamepadAvailable(n)) {
|
|
printf("Controller %d: %s\n", n, GetGamepadName(n));
|
|
}
|
|
}
|
|
|
|
// ---- And a speaker, if the host has one ----
|
|
//
|
|
// Asked for rather than assumed: a machine with no audio device is a perfectly good
|
|
// machine to look at, and a front end that refused to start without one would make
|
|
// the window depend on something the picture does not need.
|
|
InitAudioDevice();
|
|
if (IsAudioDeviceReady()) {
|
|
SetAudioStreamBufferSizeDefault(SOUND_BUFFER);
|
|
soundStream = LoadAudioStream(SOUND_SAMPLE_RATE, 16, 1);
|
|
PlayAudioStream(soundStream);
|
|
speakerOn = 1;
|
|
}
|
|
// One texture, updated in place. Making a new one every frame would be a new
|
|
// allocation sixty times a second for a picture that is the same size every time.
|
|
Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK);
|
|
ImageFormat(&blank, PIXELFORMAT_UNCOMPRESSED_R8G8B8);
|
|
screenTexture = LoadTextureFromImage(blank);
|
|
UnloadImage(blank);
|
|
windowOpen = 1;
|
|
// The keyboard becomes the console's input, in place of a standard input the window
|
|
// does not have.
|
|
consoleSetInputHook(voyagerKey);
|
|
// ---- A slice a frame ----
|
|
//
|
|
// The machine gets its turn, then the window gets its turn. Closing the window stops
|
|
// the machine, and the machine halting leaves the window up so that whatever it drew
|
|
// is still there to look at - a program that ends should not take its output off the
|
|
// screen with it.
|
|
while (windowOpen && !WindowShouldClose()) {
|
|
// Before the running check, not after it: a machine that has stopped is the one
|
|
// worth restarting, and it is the one that cannot notice a reset by itself.
|
|
machineTakeReset(&machine);
|
|
if (machineRunning(&machine)) {
|
|
machineRunSlice(&machine);
|
|
}
|
|
presentFrame();
|
|
// After the slice, so what the machine just made is what gets played.
|
|
if (speakerOn) {
|
|
feedSpeaker();
|
|
}
|
|
}
|
|
windowOpen = 0;
|
|
// Taken back before the machine stops, so nothing can ask a window that has gone.
|
|
consoleSetInputHook(NULL);
|
|
if (speakerOn) {
|
|
UnloadAudioStream(soundStream);
|
|
speakerOn = 0;
|
|
}
|
|
CloseAudioDevice();
|
|
UnloadTexture(screenTexture);
|
|
CloseWindow();
|
|
}
|
|
|
|
machineStop(&machine);
|
|
return machineReport(&machine);
|
|
}
|