mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
gameboy save states
This commit is contained in:
@@ -299,6 +299,9 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
if (mode != Mode::Running)
|
||||
break;
|
||||
|
||||
|
||||
GB_PERF_ONLY(perf.geometryUs = 0;)
|
||||
|
||||
@@ -339,6 +342,16 @@ public:
|
||||
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||
break;
|
||||
}
|
||||
case Mode::Prompt: {
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||
handlePromptInput(input);
|
||||
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
|
||||
renderPrompt();
|
||||
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prevInput = input;
|
||||
@@ -805,8 +818,9 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
enum class Mode { Browse, Running };
|
||||
enum class Mode { Browse, Running, Prompt };
|
||||
enum class ScaleMode { Original, FullHeight, FullHeightWide };
|
||||
enum class PromptKind { None, LoadState, SaveState };
|
||||
|
||||
struct PerfTracker {
|
||||
enum class CallbackKind { RomRead, CartRamRead, CartRamWrite, LcdDraw, Error };
|
||||
@@ -933,8 +947,19 @@ public:
|
||||
|
||||
void printStep(Mode stepMode, bool gbReady, bool frameDirty, uint32_t fps, const std::string& romName,
|
||||
std::size_t romCount, std::size_t selectedIdx, bool browserDirty) const {
|
||||
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
|
||||
const char* modeStr = (stepMode == Mode::Running) ? "RUN" : "BROWSE";
|
||||
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
|
||||
const char* modeStr = "UNKNOWN";
|
||||
switch (stepMode) {
|
||||
case Mode::Running:
|
||||
modeStr = "RUN";
|
||||
break;
|
||||
case Mode::Browse:
|
||||
modeStr = "BROWSE";
|
||||
break;
|
||||
case Mode::Prompt:
|
||||
modeStr = "PROMPT";
|
||||
break;
|
||||
}
|
||||
const char* name = romName.empty() ? "-" : romName.c_str();
|
||||
const std::size_t safeIdx = (romCount == 0) ? 0 : std::min(selectedIdx, romCount - 1);
|
||||
std::printf(
|
||||
@@ -1113,6 +1138,10 @@ public:
|
||||
uint32_t fpsCurrent = 0;
|
||||
std::string activeRomName;
|
||||
std::string activeRomSavePath;
|
||||
std::string activeRomStatePath;
|
||||
PromptKind promptKind = PromptKind::None;
|
||||
int promptSelection = 0; // 0 = Yes, 1 = No
|
||||
bool promptDirty = false;
|
||||
SimpleApu apu{};
|
||||
// Smoothing state for buzzer tone
|
||||
uint32_t lastFreqHz = 0;
|
||||
@@ -1451,21 +1480,25 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
gb.direct.priv = this;
|
||||
gb.direct.joypad = 0xFF;
|
||||
|
||||
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
|
||||
applyRuntimeBindings();
|
||||
|
||||
gb.direct.joypad = 0xFF;
|
||||
gb.direct.interlace = false;
|
||||
gb.direct.frame_skip = true;
|
||||
|
||||
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||
std::string savePath;
|
||||
std::string statePath;
|
||||
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
|
||||
if (fsReady)
|
||||
savePath = buildSavePath(rom, romDirectory());
|
||||
activeRomSavePath = savePath;
|
||||
if (fsReady) {
|
||||
const std::string romDir = romDirectory();
|
||||
savePath = buildSavePath(rom, romDir);
|
||||
statePath = buildStatePath(rom, romDir);
|
||||
}
|
||||
activeRomSavePath = savePath;
|
||||
activeRomStatePath = statePath;
|
||||
loadSaveFile();
|
||||
|
||||
resetFpsStats();
|
||||
@@ -1480,6 +1513,10 @@ public:
|
||||
if (!fsReady)
|
||||
statusText.append(" (no save)");
|
||||
setStatus(std::move(statusText));
|
||||
|
||||
if (stateFileExists())
|
||||
enterPrompt(PromptKind::LoadState);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1492,6 +1529,10 @@ public:
|
||||
cartRam.clear();
|
||||
activeRomName.clear();
|
||||
activeRomSavePath.clear();
|
||||
activeRomStatePath.clear();
|
||||
promptKind = PromptKind::None;
|
||||
promptSelection = 0;
|
||||
promptDirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1505,6 +1546,10 @@ public:
|
||||
cartRam.clear();
|
||||
activeRomName.clear();
|
||||
activeRomSavePath.clear();
|
||||
activeRomStatePath.clear();
|
||||
promptKind = PromptKind::None;
|
||||
promptSelection = 0;
|
||||
promptDirty = false;
|
||||
std::memset(&gb, 0, sizeof(gb));
|
||||
apu.reset();
|
||||
mode = Mode::Browse;
|
||||
@@ -1519,6 +1564,19 @@ public:
|
||||
if (scaleToggleCombo)
|
||||
toggleScaleMode();
|
||||
|
||||
const bool exitComboPressed = input.start && input.select;
|
||||
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
|
||||
if (exitComboJustPressed) {
|
||||
if (!activeRomStatePath.empty()) {
|
||||
enterPrompt(PromptKind::SaveState);
|
||||
} else {
|
||||
unloadRom();
|
||||
setStatus("Save state unavailable");
|
||||
}
|
||||
gb.direct.joypad = 0xFF;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t joypad = 0xFF;
|
||||
if (input.a)
|
||||
joypad &= ~JOYPAD_A;
|
||||
@@ -1538,13 +1596,6 @@ public:
|
||||
joypad &= ~JOYPAD_RIGHT;
|
||||
|
||||
gb.direct.joypad = joypad;
|
||||
|
||||
const bool exitComboPressed = input.start && input.select;
|
||||
const bool exitComboJustPressed = exitComboPressed && !(prevInput.start && prevInput.select);
|
||||
if (exitComboJustPressed) {
|
||||
setStatus("Saved " + activeRomName);
|
||||
unloadRom();
|
||||
}
|
||||
}
|
||||
|
||||
void renderGameFrame() {
|
||||
@@ -1716,6 +1767,183 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void applyRuntimeBindings() {
|
||||
gb.gb_cart_ram_read = &GameboyApp::cartRamRead;
|
||||
gb.gb_cart_ram_write = &GameboyApp::cartRamWrite;
|
||||
gb.gb_error = &GameboyApp::errorCallback;
|
||||
gb.display.lcd_draw_line = &GameboyApp::lcdDrawLine;
|
||||
gb.direct.priv = this;
|
||||
if (romDataView)
|
||||
gb.direct.rom = romDataView;
|
||||
}
|
||||
|
||||
bool saveStateToFile() {
|
||||
if (activeRomStatePath.empty())
|
||||
return false;
|
||||
|
||||
FILE* file = std::fopen(activeRomStatePath.c_str(), "wb");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
const size_t written = std::fwrite(&gb, 1, sizeof(gb), file);
|
||||
std::fclose(file);
|
||||
return written == sizeof(gb);
|
||||
}
|
||||
|
||||
bool loadStateFromFile() {
|
||||
if (activeRomStatePath.empty())
|
||||
return false;
|
||||
|
||||
FILE* file = std::fopen(activeRomStatePath.c_str(), "rb");
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
struct gb_s restored{};
|
||||
const size_t read = std::fread(&restored, 1, sizeof(restored), file);
|
||||
std::fclose(file);
|
||||
if (read != sizeof(restored))
|
||||
return false;
|
||||
|
||||
gb = restored;
|
||||
applyRuntimeBindings();
|
||||
gb.direct.joypad = 0xFF;
|
||||
frameDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool stateFileExists() const {
|
||||
if (activeRomStatePath.empty())
|
||||
return false;
|
||||
struct stat st{};
|
||||
return stat(activeRomStatePath.c_str(), &st) == 0 && S_ISREG(st.st_mode);
|
||||
}
|
||||
|
||||
void enterPrompt(PromptKind kind) {
|
||||
if (kind == PromptKind::None)
|
||||
return;
|
||||
promptKind = kind;
|
||||
promptSelection = 0;
|
||||
promptDirty = true;
|
||||
mode = Mode::Prompt;
|
||||
gb.direct.joypad = 0xFF;
|
||||
scheduleNextTick(0);
|
||||
}
|
||||
|
||||
void exitPrompt(Mode nextMode) {
|
||||
promptKind = PromptKind::None;
|
||||
promptSelection = 0;
|
||||
promptDirty = true;
|
||||
mode = nextMode;
|
||||
}
|
||||
|
||||
void handlePromptInput(const InputState& input) {
|
||||
if (promptKind == PromptKind::None)
|
||||
return;
|
||||
|
||||
const bool leftOrUp = (input.left && !prevInput.left) || (input.up && !prevInput.up);
|
||||
const bool rightOrDown = (input.right && !prevInput.right) || (input.down && !prevInput.down);
|
||||
if (leftOrUp && promptSelection != 0) {
|
||||
promptSelection = 0;
|
||||
promptDirty = true;
|
||||
} else if (rightOrDown && promptSelection != 1) {
|
||||
promptSelection = 1;
|
||||
promptDirty = true;
|
||||
}
|
||||
|
||||
const bool confirm = (input.a && !prevInput.a) || (input.start && !prevInput.start);
|
||||
const bool cancel = (input.b && !prevInput.b) || (input.select && !prevInput.select);
|
||||
|
||||
if (confirm) {
|
||||
handlePromptDecision(promptSelection == 0);
|
||||
} else if (cancel) {
|
||||
handlePromptDecision(false);
|
||||
}
|
||||
}
|
||||
|
||||
void handlePromptDecision(bool yesSelected) {
|
||||
const PromptKind kind = promptKind;
|
||||
if (kind == PromptKind::None)
|
||||
return;
|
||||
|
||||
if (kind == PromptKind::LoadState) {
|
||||
exitPrompt(Mode::Running);
|
||||
if (yesSelected) {
|
||||
if (loadStateFromFile())
|
||||
setStatus("Save state loaded");
|
||||
else
|
||||
setStatus("Load state failed");
|
||||
}
|
||||
frameDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind == PromptKind::SaveState) {
|
||||
const bool haveStatePath = !activeRomStatePath.empty();
|
||||
bool saved = false;
|
||||
if (yesSelected && haveStatePath)
|
||||
saved = saveStateToFile();
|
||||
|
||||
exitPrompt(Mode::Running);
|
||||
unloadRom();
|
||||
|
||||
if (yesSelected) {
|
||||
if (!haveStatePath)
|
||||
setStatus("Save state unavailable");
|
||||
else if (saved)
|
||||
setStatus("Save state written");
|
||||
else
|
||||
setStatus("Save state failed");
|
||||
} else {
|
||||
setStatus("Exited without state");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void renderPrompt() {
|
||||
if (!promptDirty || promptKind == PromptKind::None)
|
||||
return;
|
||||
promptDirty = false;
|
||||
|
||||
framebuffer.frameReady();
|
||||
framebuffer.clear(false);
|
||||
|
||||
auto drawCentered = [&](int y, std::string_view text, int scale = 1) {
|
||||
const int width = font16x8::measureText(text, scale, 1);
|
||||
const int x = (framebuffer.width() - width) / 2;
|
||||
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
|
||||
};
|
||||
|
||||
std::string_view headline;
|
||||
std::string_view helper;
|
||||
|
||||
switch (promptKind) {
|
||||
case PromptKind::LoadState:
|
||||
headline = "LOAD SAVE STATE?";
|
||||
helper = "A YES / B NO";
|
||||
break;
|
||||
case PromptKind::SaveState:
|
||||
headline = "SAVE BEFORE EXIT?";
|
||||
helper = "A YES / B NO";
|
||||
break;
|
||||
case PromptKind::None:
|
||||
default:
|
||||
headline = "";
|
||||
helper = "";
|
||||
break;
|
||||
}
|
||||
|
||||
drawCentered(40, headline);
|
||||
drawCentered(72, helper);
|
||||
|
||||
const std::string yesLabel = (promptSelection == 0) ? "> YES" : " YES";
|
||||
const std::string noLabel = (promptSelection == 1) ? "> NO" : " NO";
|
||||
|
||||
drawCentered(104, yesLabel);
|
||||
drawCentered(120, noLabel);
|
||||
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
|
||||
void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) {
|
||||
if (freqHz == 0 || durationMs == 0)
|
||||
return;
|
||||
@@ -1767,6 +1995,21 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string buildStatePath(const RomEntry& rom, std::string_view romDir) {
|
||||
std::string slug = rom.saveSlug;
|
||||
if (slug.empty())
|
||||
slug = sanitizeSaveSlug(rom.name);
|
||||
if (slug.empty())
|
||||
slug = "rom";
|
||||
|
||||
std::string result(romDir);
|
||||
if (!result.empty() && result.back() != '/')
|
||||
result.push_back('/');
|
||||
result.append(slug);
|
||||
result.append(".state");
|
||||
return result;
|
||||
}
|
||||
|
||||
static GameboyApp* fromGb(struct gb_s* gb) {
|
||||
CARDBOY_CHECK_CODE(if (!gb) return nullptr;);
|
||||
return static_cast<GameboyApp*>(gb->direct.priv);
|
||||
|
||||
Reference in New Issue
Block a user