Skip to content

Add named save states widget, disable menu entries for numbered/quick… #1466

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 23, 2023
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
64 changes: 51 additions & 13 deletions src/gui/gui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ void PCSX::GUI::endFrame() {
SaveStates::ProtoFile::dumpSchema(schema);
}

static constexpr char globalSaveState[] = "global.sstate";
const auto globalSaveStateName = fmt::format(f_("global{}"), getSaveStatePostfix());

if (ImGui::BeginMenu(_("Save state slots"))) {
if (ImGui::MenuItem(_("Quick-save slot"), "F1")) {
Expand All @@ -1079,25 +1079,44 @@ void PCSX::GUI::endFrame() {
}
}

ImGui::Separator();
ImGui::MenuItem(_("Show named save states"), nullptr, &m_namedSaveStates.m_show);

ImGui::EndMenu();
}

if (ImGui::MenuItem(_("Save global state"))) {
saveSaveState(globalSaveState);
}
if (ImGui::MenuItem(_("Save global state"))) saveSaveState(globalSaveStateName);

if (ImGui::BeginMenu(_("Load state slots"))) {
if (ImGui::MenuItem(_("Quick-save slot"), "F2")) loadSaveState(buildSaveStateFilename(0));
auto saveFilename = buildSaveStateFilename(0);
ImGui::BeginDisabled(!saveStateExists(saveFilename));
if (ImGui::MenuItem(_("Quick-load slot"), "F2")) loadSaveState(saveFilename);
ImGui::EndDisabled();

for (auto i = 1; i < 10; i++) {
const auto str = fmt::format(f_("Slot {}"), i);
if (ImGui::MenuItem(str.c_str())) loadSaveState(buildSaveStateFilename(i));
saveFilename = buildSaveStateFilename(i);
ImGui::BeginDisabled(!saveStateExists(saveFilename));
if (ImGui::MenuItem(str.c_str())) loadSaveState(saveFilename);
ImGui::EndDisabled();
}

auto namedSaves = m_namedSaveStates.getNamedSaveStates(this);
if (!namedSaves.empty()) {
ImGui::Separator();
for (auto filenamePair : namedSaves) {
if (ImGui::MenuItem(filenamePair.second.c_str())) {
loadSaveState(filenamePair.first);
}
}
}

ImGui::EndMenu();
}

if (ImGui::MenuItem(_("Load global state"))) loadSaveState(globalSaveState);
ImGui::BeginDisabled(!saveStateExists(globalSaveStateName));
if (ImGui::MenuItem(_("Load global state"))) loadSaveState(globalSaveStateName);
ImGui::EndDisabled();

ImGui::Separator();
if (ImGui::MenuItem(_("Open LID"))) {
Expand Down Expand Up @@ -1469,6 +1488,10 @@ in Configuration->Emulation, restart PCSX-Redux, then try again.)"));
m_breakpoints.draw(_("Breakpoints"));
}

if (m_namedSaveStates.m_show) {
m_namedSaveStates.draw(this, _("Named Save States"));
}

if (m_memoryObserver.m_show) {
m_memoryObserver.draw(_("Memory Observer"));
}
Expand Down Expand Up @@ -2431,24 +2454,31 @@ void PCSX::GUI::magicOpen(const char* pathStr) {
g_emulator->m_cdrom->check();
}

std::string PCSX::GUI::buildSaveStateFilename(int i) {
std::string PCSX::GUI::getSaveStatePrefix(bool includeSeparator) {
// the ID of the game. Every savestate is marked with the ID of the game it's from.
const auto gameID = g_emulator->m_cdrom->getCDRomID();

// Check if the game has a non-NULL ID or a game hasn't been loaded. Some stuff like PS-X
// EXEs don't have proper IDs
if (!gameID.empty()) {
// For games with an ID of SLUS00213 for example, this will generate a state named
// SLUS00213.sstate
return fmt::format("{}.sstate{}", gameID, i);
return fmt::format("{}{}", gameID, includeSeparator ? "_" : "");
} else {
// For games without IDs, identify them via filename
const auto& iso = PCSX::g_emulator->m_cdrom->getIso()->getIsoPath().filename();
const auto& iso = g_emulator->m_cdrom->getIso()->getIsoPath().filename();
const auto lastFile = iso.empty() ? "BIOS" : iso.string();
return fmt::format("{}.sstate{}", lastFile, i);
return fmt::format("{}{}", lastFile, includeSeparator ? "_" : "");
}
}

std::string PCSX::GUI::getSaveStatePostfix() {
return ".sstate";
}

std::string PCSX::GUI::buildSaveStateFilename(int i) {
return fmt::format("{}{}{}", getSaveStatePrefix(false), getSaveStatePostfix(), i);
}

void PCSX::GUI::saveSaveState(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
Expand Down Expand Up @@ -2484,7 +2514,15 @@ void PCSX::GUI::loadSaveState(std::filesystem::path filename) {
delete[] buff;

if (!error) SaveStates::load(os.str());
};
}

bool PCSX::GUI::saveStateExists(std::filesystem::path filename) {
if (filename.is_relative()) {
filename = g_system->getPersistentDir() / filename;
}
ZReader save(new PosixFile(filename));
return !save.failed();
}

void PCSX::GUI::byteRateToString(float rate, std::string& str) {
if (rate >= 1000000000) {
Expand Down
13 changes: 11 additions & 2 deletions src/gui/gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "gui/widgets/luaeditor.h"
#include "gui/widgets/luainspector.h"
#include "gui/widgets/memcard_manager.h"
#include "gui/widgets/named_savestates.h"
#include "gui/widgets/pio-cart.h"
#include "gui/widgets/registers.h"
#include "gui/widgets/shader-editor.h"
Expand Down Expand Up @@ -98,6 +99,7 @@ class GUI final : public UI {
typedef Setting<bool, TYPESTRING("ShowAssembly")> ShowAssembly;
typedef Setting<bool, TYPESTRING("ShowDisassembly")> ShowDisassembly;
typedef Setting<bool, TYPESTRING("ShowBreakpoints")> ShowBreakpoints;
typedef Setting<bool, TYPESTRING("ShowNamedSaveStates")> ShowNamedSaveStates;
typedef Setting<bool, TYPESTRING("ShowEvents")> ShowEvents;
typedef Setting<bool, TYPESTRING("ShowHandlers")> ShowHandlers;
typedef Setting<bool, TYPESTRING("ShowKernelLog")> ShowKernelLog;
Expand Down Expand Up @@ -146,8 +148,8 @@ class GUI final : public UI {
Settings<Fullscreen, FullWindowRender, ShowMenu, ShowLog, WindowPosX, WindowPosY, WindowSizeX, WindowSizeY,
IdleSwapInterval, ShowLuaConsole, ShowLuaInspector, ShowLuaEditor, ShowMainVRAMViewer, ShowCLUTVRAMViewer,
ShowVRAMViewer1, ShowVRAMViewer2, ShowVRAMViewer3, ShowVRAMViewer4, ShowMemoryObserver, ShowTypedDebugger,
ShowMemcardManager, ShowRegisters, ShowAssembly, ShowDisassembly, ShowBreakpoints, ShowEvents,
ShowHandlers, ShowKernelLog, ShowCallstacks, ShowSIO1, ShowIsoBrowser, ShowGPULogger, MainFontSize,
ShowMemcardManager, ShowRegisters, ShowAssembly, ShowDisassembly, ShowBreakpoints, ShowNamedSaveStates,
ShowEvents, ShowHandlers, ShowKernelLog, ShowCallstacks, ShowSIO1, ShowIsoBrowser, ShowGPULogger, MainFontSize,
MonoFontSize, GUITheme, AllowMouseCaptureToggle, EnableRawMouseMotion, WidescreenRatio, ShowPIOCartConfig,
ShowMemoryEditor1, ShowMemoryEditor2, ShowMemoryEditor3, ShowMemoryEditor4, ShowMemoryEditor5,
ShowMemoryEditor6, ShowMemoryEditor7, ShowMemoryEditor8, ShowParallelPortEditor, ShowScratchpadEditor,
Expand Down Expand Up @@ -375,6 +377,7 @@ class GUI final : public UI {
Widgets::FileDialog<> m_openBinaryDialog = {[]() { return _("Open Binary"); }};
Widgets::FileDialog<> m_selectBiosDialog = {[]() { return _("Select BIOS"); }};
Widgets::FileDialog<> m_selectEXP1Dialog = {[]() { return _("Select EXP1"); }};
Widgets::NamedSaveStates m_namedSaveStates = {settings.get<ShowNamedSaveStates>().value};
Widgets::Breakpoints m_breakpoints = {settings.get<ShowBreakpoints>().value};
Widgets::IsoBrowser m_isoBrowser = {settings.get<ShowIsoBrowser>().value};

Expand Down Expand Up @@ -405,9 +408,15 @@ class GUI final : public UI {
EventBus::Listener m_listener;

std::string buildSaveStateFilename(int i);
bool saveStateExists(std::filesystem::path filename);

public:
void saveSaveState(std::filesystem::path filename);
void loadSaveState(std::filesystem::path filename);
std::string getSaveStatePrefix(bool includeSeparator);
static std::string getSaveStatePostfix();

private:
void applyTheme(int theme);
void cherryTheme();
void monoTheme();
Expand Down
184 changes: 184 additions & 0 deletions src/gui/widgets/named_savestates.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@

#include "gui/widgets/named_savestates.h"

#include "core/cdrom.h"
#include "core/debug.h"
#include "gui/gui.h"
#include "imgui.h"
#include "imgui_internal.h"

void PCSX::Widgets::NamedSaveStates::draw(GUI* gui, const char* title) {
ImGui::SetNextWindowPos(ImVec2(520, 30), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
if (!ImGui::Begin(title, &m_show)) {
ImGui::End();
return;
}

ImGuiStyle& style = ImGui::GetStyle();
const float heightSeparator = style.ItemSpacing.y;
const float footerHeight = (heightSeparator * 2 + ImGui::GetTextLineHeightWithSpacing()) * 4;
const float glyphWidth = ImGui::GetFontSize();

// Any ImGui::Text() preceeding a ImGui::InputText() will be incorrectly aligned, use this value to awkwardly account for this
// Luckily this value is consistent no matter what the UI main font size is set to
const float verticalAlignAdjust = 3.0f;

// Create a button for each named save state
ImGui::BeginChild("SaveStatesList", ImVec2(0, -footerHeight), true);
auto saveStates = getNamedSaveStates(gui);
for (const auto& saveStatePair : saveStates) {
// The button is invisible so we can adjust the highlight separately
if (ImGui::InvisibleButton(saveStatePair.second.c_str(),
ImVec2(-FLT_MIN, glyphWidth + style.FramePadding.y * 2),
ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick)) {
// Copy the name to the input box
strcpy(m_namedSaveNameString, saveStatePair.second.c_str());
// Load the save state if it was a double-click
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
loadSaveState(gui, saveStatePair.first);
}
}
// Add a base coloured rect - highlight it if hovering over the button or the current InputText below matches it
bool matches = StringsHelpers::strcasecmp(m_namedSaveNameString, saveStatePair.second.c_str());
bool hovered = ImGui::IsItemHovered();
ImGui::GetWindowDrawList()->AddRectFilled(
ImGui::GetCurrentContext()->LastItemData.Rect.Min, ImGui::GetCurrentContext()->LastItemData.Rect.Max,
ImGui::ColorConvertFloat4ToU32(
ImGui::GetStyle()
.Colors[matches ? ImGuiCol_HeaderActive : (hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header)]));
// Finally the butotn text
ImGui::GetWindowDrawList()->AddText(
ImVec2(ImGui::GetCurrentContext()->LastItemData.Rect.Min.x + style.FramePadding.y,
ImGui::GetCurrentContext()->LastItemData.Rect.Min.y + style.FramePadding.y),
ImGui::GetColorU32(ImGuiCol_Text), saveStatePair.second.c_str());
}
ImGui::EndChild();

// Move the leading text down to align with the following InputText()
float posY = ImGui::GetCursorPosY();
ImGui::SetCursorPosY(posY + verticalAlignAdjust);

ImGui::Text(_("Filename: "));
ImGui::SameLine();
ImGui::Text(gui->getSaveStatePrefix(true).c_str());
ImGui::SameLine(0.0f, 0.0f);

// Restore the vertical value
ImGui::SetCursorPosY(posY);

// Ensure that we don't add invalid characters to the filename
// This also filters on pasted text
struct TextFilters {
static int FilterNonPathCharacters(ImGuiInputTextCallbackData* data) {
// Filter the core problematic characters for Windows and Linux
// Anything remaining outside of [a-zA-Z0-9._-] is also allowed
switch (data->EventChar) {
case '\\':
case '/':
case '<':
case '>':
case '|':
case '\"':
case ':':
case '?':
case '*':
case 0:
return 1;
}
return 0;
}
};

ImGui::InputTextWithHint("##SaveNameInput", "Enter the name of your save state here", m_namedSaveNameString,
NAMED_SAVE_STATE_LENGTH_MAX, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterNonPathCharacters);
ImGui::SameLine(0.0f, 0.0f);

// Trailing text alignment also needs adjusting, but in the opposite direction
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - verticalAlignAdjust);
ImGui::Text(gui->getSaveStatePostfix().c_str());

ImGui::Separator();

// Add various buttons based on whether a save state exists with that name
auto found = std::find_if(saveStates.begin(), saveStates.end(), [=](auto saveStatePair) {
return strlen(m_namedSaveNameString) > 0 &&
StringsHelpers::strcasecmp(m_namedSaveNameString, saveStatePair.second.c_str());
});
bool exists = found != saveStates.end();

float width = ImGui::GetCurrentContext()->LastItemData.Rect.GetWidth();
ImVec2 buttonDims = ImVec2(-FLT_MIN, glyphWidth + style.FramePadding.y * 2);
ImVec2 halfDims = ImVec2((width - (style.FramePadding.x * 6.0f)) * 0.5f, buttonDims.y);

if (!exists) {
if (strlen(m_namedSaveNameString) > 0) {
// The save state doesn't exist, and the name is valid
std::string pathStr = fmt::format("{}{}{}", gui->getSaveStatePrefix(true), m_namedSaveNameString, gui->getSaveStatePostfix());
std::filesystem::path newPath = pathStr;
if (ImGui::Button(_("Create save"), buttonDims)) {
saveSaveState(gui, newPath);
}
}
} else {
// The save state exists
if (ImGui::Button(_("Overwrite save"), halfDims)) {
saveSaveState(gui, found->first);
}
ImGui::SameLine();
if (ImGui::Button(_("Load save"), halfDims)) {
loadSaveState(gui, found->first);
}
// Add a deliberate spacer before the delete button
// There is no delete confirmation, and this makes a mis-click less likely to hit it
ImGui::Dummy(buttonDims);
if (ImGui::Button(_("Delete save"), buttonDims)) {
deleteSaveState(found->first);
}
}

ImGui::End();
}

std::vector<std::pair<std::filesystem::path, std::string>> PCSX::Widgets::NamedSaveStates::getNamedSaveStates(GUI* gui) {
std::vector<std::pair<std::filesystem::path, std::string>> names;

// Get the filename prefix to use, which follows the typical save state format, with a separator between gameID and name
std::string prefix = gui->getSaveStatePrefix(true);
std::string postfix = gui->getSaveStatePostfix();

// Loop the root directory
std::error_code ec;
if (std::filesystem::exists(std::filesystem::current_path(), ec)) {
for (const auto& entry : std::filesystem::directory_iterator(std::filesystem::current_path(), ec)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().filename().string();
if (filename.find(prefix) == 0 &&
filename.rfind(postfix) == filename.length() - postfix.length()) {
std::string niceName = filename.substr(prefix.length(), filename.length() - (prefix.length() + postfix.length()));
// Only support names that fit within the character limit
if (niceName.length() < NAMED_SAVE_STATE_LENGTH_MAX) {
names.emplace_back(entry.path(), niceName);
}
}
}
}
}

return names;
}

void PCSX::Widgets::NamedSaveStates::saveSaveState(GUI* gui, std::filesystem::path saveStatePath) {
g_system->log(LogClass::UI, "Saving named save state: %s\n", saveStatePath.filename().string().c_str());
gui->saveSaveState(saveStatePath);
}

void PCSX::Widgets::NamedSaveStates::loadSaveState(GUI* gui, std::filesystem::path saveStatePath) {
g_system->log(LogClass::UI, "Loading named save state: %s\n", saveStatePath.filename().string().c_str());
gui->loadSaveState(saveStatePath);
}

void PCSX::Widgets::NamedSaveStates::deleteSaveState(std::filesystem::path saveStatePath) {
g_system->log(LogClass::UI, "Deleting named save state: %s\n", saveStatePath.filename().string().c_str());
std::remove(saveStatePath.string().c_str());
}
34 changes: 34 additions & 0 deletions src/gui/widgets/named_savestates.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

#pragma once

#include <string>
#include <vector>
#include <filesystem>

namespace PCSX {

class GUI;
namespace Widgets {

class NamedSaveStates {
public:
NamedSaveStates(bool& show) : m_show(show) {}
void draw(GUI* gui, const char* title);
bool& m_show;

std::vector<std::pair<std::filesystem::path, std::string>> getNamedSaveStates(GUI* gui);

private:

static constexpr int NAMED_SAVE_STATE_LENGTH_MAX = 128;

void saveSaveState(GUI* gui, std::filesystem::path saveStatePath);
void loadSaveState(GUI* gui, std::filesystem::path saveStatePath);
void deleteSaveState(std::filesystem::path saveStatePath);

char m_namedSaveNameString[NAMED_SAVE_STATE_LENGTH_MAX] = "";
};

} // namespace Widgets

} // namespace PCSX
Loading