mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-25 03:09:13 +01:00
Merge pull request #21111 from hrydgard/savestate-refactor
Some savestate code refactoring
This commit is contained in:
72 files changed
+590
-487
No files matched your search
@@ -2475,6 +2475,8 @@ add_library(${CoreLibName} ${CoreLinkType}
|
||||
Core/Replay.h
|
||||
Core/SaveState.cpp
|
||||
Core/SaveState.h
|
||||
Core/SaveStateRewind.cpp
|
||||
Core/SaveStateRewind.h
|
||||
Core/Screenshot.cpp
|
||||
Core/Screenshot.h
|
||||
Core/System.cpp
|
||||
|
||||
@@ -39,7 +39,7 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) {
|
||||
|
||||
openContentUri = env->GetStaticMethodID(localClass, "openContentUri", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
|
||||
_dbg_assert_(openContentUri);
|
||||
listContentUriDir = env->GetStaticMethodID(localClass, "listContentUriDir", "(Landroid/app/Activity;Ljava/lang/String;)[Ljava/lang/String;");
|
||||
listContentUriDir = env->GetStaticMethodID(localClass, "listContentUriDir", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;");
|
||||
_dbg_assert_(listContentUriDir);
|
||||
contentUriCreateDirectory = env->GetStaticMethodID(localClass, "contentUriCreateDirectory", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
|
||||
_dbg_assert_(contentUriCreateDirectory);
|
||||
@@ -265,7 +265,8 @@ std::vector<File::FileInfo> Android_ListContentUri(const std::string &uri, const
|
||||
double start = time_now_d();
|
||||
|
||||
jstring param = env->NewStringUTF(uri.c_str());
|
||||
jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param);
|
||||
jstring filenamePrefix = prefix.empty() ? nullptr : env->NewStringUTF(prefix.c_str());
|
||||
jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param, filenamePrefix);
|
||||
|
||||
jobjectArray fileList = (jobjectArray)retval;
|
||||
std::vector<File::FileInfo> items;
|
||||
|
||||
@@ -517,7 +517,7 @@ bool IsDirectory(const Path &path) {
|
||||
|
||||
// Deletes a given filename, return true on success
|
||||
// Doesn't supports deleting a directory
|
||||
bool Delete(const Path &filename) {
|
||||
bool Delete(const Path &filename, bool quiet) {
|
||||
if (SIMULATE_SLOW_IO) {
|
||||
sleep_ms(200, "slow-io-sim");
|
||||
}
|
||||
@@ -533,7 +533,9 @@ bool Delete(const Path &filename) {
|
||||
// Return true because we care about the file no
|
||||
// being there, not the actual delete.
|
||||
if (!Exists(filename)) {
|
||||
WARN_LOG(Log::IO, "Delete: '%s' already does not exist", filename.c_str());
|
||||
if (!quiet) {
|
||||
WARN_LOG(Log::IO, "Delete: '%s' already does not exist", filename.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ bool CreateFullPath(const Path &fullPath);
|
||||
|
||||
// Deletes a given file by name, return true on success
|
||||
// Doesn't support deleting a directory (although it will work on some platforms - ideally shouldn't)
|
||||
bool Delete(const Path &filename);
|
||||
bool Delete(const Path &filename, bool quiet = false);
|
||||
|
||||
// Deletes a directory by name, returns true on success
|
||||
// Directory must be empty.
|
||||
|
||||
+1
-1
@@ -1070,7 +1070,7 @@ const FontStyle *SimpleTextView::ComputeStyle(const UIContext &dc) const {
|
||||
}
|
||||
}
|
||||
|
||||
const FontStyle *TextView::GetTextStyle(const UIContext &dc, TextSize size) {
|
||||
const FontStyle *GetTextStyle(const UIContext &dc, TextSize size) {
|
||||
const FontStyle *style = &dc.GetTheme().uiFont;
|
||||
switch (size) {
|
||||
case TextSize::Tiny:
|
||||
|
||||
+1
-2
@@ -1047,8 +1047,6 @@ public:
|
||||
bool CanBeFocused() const override { return focusable_; }
|
||||
|
||||
private:
|
||||
static const FontStyle *GetTextStyle(const UIContext &dc, TextSize size);
|
||||
|
||||
std::string text_;
|
||||
int textAlign_;
|
||||
uint32_t textColor_;
|
||||
@@ -1190,5 +1188,6 @@ bool IsTabRightKey(const KeyInput &key);
|
||||
|
||||
// TODO: Doesn't really belong here.
|
||||
void DrawIconShine(UIContext &dc, const Bounds &bounds, float shine, bool animated);
|
||||
const FontStyle *GetTextStyle(const UIContext &dc, TextSize size);
|
||||
|
||||
} // namespace
|
||||
@@ -926,6 +926,7 @@
|
||||
<ClCompile Include="RetroAchievements.cpp" />
|
||||
<ClCompile Include="SaveState.cpp" />
|
||||
<ClCompile Include="MIPS\MIPSStackWalk.cpp" />
|
||||
<ClCompile Include="SaveStateRewind.cpp" />
|
||||
<ClCompile Include="Screenshot.cpp" />
|
||||
<ClCompile Include="System.cpp" />
|
||||
<ClCompile Include="TiltEventProcessor.cpp" />
|
||||
@@ -1295,6 +1296,7 @@
|
||||
<ClInclude Include="RetroAchievements.h" />
|
||||
<ClInclude Include="SaveState.h" />
|
||||
<ClInclude Include="MIPS\MIPSStackWalk.h" />
|
||||
<ClInclude Include="SaveStateRewind.h" />
|
||||
<ClInclude Include="Screenshot.h" />
|
||||
<ClInclude Include="System.h" />
|
||||
<ClInclude Include="TiltEventProcessor.h" />
|
||||
|
||||
@@ -1396,6 +1396,9 @@
|
||||
<ClCompile Include="Util\PathUtil.cpp">
|
||||
<Filter>Util</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SaveStateRewind.cpp">
|
||||
<Filter>Core</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ELF\ElfReader.h">
|
||||
@@ -2256,6 +2259,9 @@
|
||||
<ClInclude Include="Util\PathUtil.h">
|
||||
<Filter>Util</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SaveStateRewind.h">
|
||||
<Filter>Core</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE.TXT" />
|
||||
|
||||
@@ -648,4 +648,8 @@ enum PSPErrorCode : u32 {
|
||||
SCE_NET_ADHOCCTL_ERROR_CHANNEL_NOT_MATCH = 0x80410b11,
|
||||
SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS = 0x80410b12,
|
||||
SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT = 0x80410B13,
|
||||
|
||||
// Error codes matching JPCSP
|
||||
SCE_NPDRM_ERROR_NO_K_LICENSEE_SET = 0x80550901,
|
||||
SCE_NPDRM_ERROR_INVALID_FILE = 0x80550902,
|
||||
};
|
||||
+1
-1
@@ -2589,7 +2589,7 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out
|
||||
key_ptr = nullptr;
|
||||
}
|
||||
|
||||
DEBUG_LOG(Log::sceIo, "Decrypting PGD DRM files");
|
||||
INFO_LOG(Log::sceIo, "ioctl: Decrypting PGD DRM files from '%s'", f->fullpath.c_str());
|
||||
pspFileSystem.SeekFile(f->handle, (s32)f->pgd_offset, FILEMOVE_BEGIN);
|
||||
pspFileSystem.ReadFile(f->handle, pgd_header, 0x90);
|
||||
KirkState *kirk = __ChnnlsvKirkState();
|
||||
|
||||
@@ -1633,6 +1633,10 @@ const char *KernelErrorToString(u32 err) {
|
||||
case SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS: return "SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS";
|
||||
case SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT: return "SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT";
|
||||
|
||||
// NPDRM
|
||||
case SCE_NPDRM_ERROR_NO_K_LICENSEE_SET: return "SCE_NPDRM_ERROR_NO_K_LICENSEE_SET";
|
||||
case SCE_NPDRM_ERROR_INVALID_FILE: return "SCE_NPDRM_ERROR_INVALID_FILE";
|
||||
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "scePspNpDrm_user.h"
|
||||
|
||||
#include "Core/HLE/scePspNpDrm_user.h"
|
||||
#include "Core/MemMapHelpers.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
#include "Core/HLE/FunctionWrappers.h"
|
||||
@@ -13,10 +12,6 @@ static const int PSP_NPDRM_LICENSEE_KEY_LENGTH = 0x10;
|
||||
static u8 licenseeKey[PSP_NPDRM_LICENSEE_KEY_LENGTH];
|
||||
static bool isLicenseeKeySet = false;
|
||||
|
||||
// Error codes matching JPCSP
|
||||
static const int ERROR_NPDRM_NO_K_LICENSEE_SET = 0x80550901;
|
||||
static const int ERROR_NPDRM_INVALID_FILE = 0x80550902;
|
||||
|
||||
// Check if the file is an encrypted EDAT file by reading the magic number
|
||||
static bool isEncrypted(u32 edataFd) {
|
||||
// Check for "\0PSPEDAT" magic number
|
||||
@@ -67,7 +62,7 @@ static int sceNpDrmSetLicenseeKey(u32 npDrmKeyAddr) {
|
||||
isLicenseeKeySet = true;
|
||||
return hleLogInfo(Log::sceIo, 0);
|
||||
}
|
||||
return hleLogError(Log::sceIo, ERROR_NPDRM_INVALID_FILE, "Invalid address");
|
||||
return hleLogError(Log::sceIo, SCE_NPDRM_ERROR_INVALID_FILE, "Invalid address");
|
||||
}
|
||||
|
||||
static int sceNpDrmClearLicenseeKey() {
|
||||
@@ -85,7 +80,7 @@ static int sceNpDrmEdataSetupKey(u32 edataFd) {
|
||||
// Return an error if the key has not been set.
|
||||
// Note: An empty key is valid, as long as it was set with sceNpDrmSetLicenseeKey.
|
||||
if (!isLicenseeKeySet) {
|
||||
return hleLogError(Log::sceIo, ERROR_NPDRM_NO_K_LICENSEE_SET, "Licensee key not set");
|
||||
return hleLogError(Log::sceIo, SCE_NPDRM_ERROR_NO_K_LICENSEE_SET, "Licensee key not set");
|
||||
}
|
||||
|
||||
// Get file info to validate the file descriptor
|
||||
@@ -129,8 +124,7 @@ static int sceNpDrmEdataGetDataSize(u32 edataFd) {
|
||||
}
|
||||
|
||||
static int sceNpDrmOpen() {
|
||||
ERROR_LOG(Log::sceIo, "UNIMPL: sceNpDrmOpen()");
|
||||
return 0;
|
||||
return hleLogError(Log::sceIo, 0, "UNIMPL: sceNpDrmOpen()");
|
||||
}
|
||||
|
||||
const HLEFunction sceNpDrm[] = {
|
||||
|
||||
+128
-334
@@ -26,6 +26,7 @@
|
||||
#include "Common/System/System.h"
|
||||
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/DirListing.h"
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
#include "Common/Serialize/SerializeFuncs.h"
|
||||
#include "Common/StringUtils.h"
|
||||
@@ -37,6 +38,7 @@
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Screenshot.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/SaveStateRewind.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
#include "Core/ELF/ParamSFO.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
@@ -46,11 +48,9 @@
|
||||
#include "Core/HLE/sceKernel.h"
|
||||
#include "Core/HLE/sceUtility.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/MIPS/MIPS.h"
|
||||
#include "Core/MIPS/JitCommon/JitBlockCache.h"
|
||||
#include "Core/RetroAchievements.h"
|
||||
#include "HW/MemoryStick.h"
|
||||
#include "GPU/GPUState.h"
|
||||
|
||||
#ifndef MOBILE_DEVICE
|
||||
#include "Core/AVIDump.h"
|
||||
@@ -59,24 +59,44 @@
|
||||
|
||||
// Slot number is visual only, -2 will display special message
|
||||
constexpr int LOAD_UNDO_SLOT = -2;
|
||||
constexpr int SCREENSHOT_FAILURE_RETRIES = 6;
|
||||
|
||||
static const char * const STATE_EXTENSION = "ppst";
|
||||
static const char * const UNDO_STATE_EXTENSION = "undo.ppst";
|
||||
static const char * const UNDO_SCREENSHOT_EXTENSION = "undo.jpg";
|
||||
|
||||
static const char * const LOAD_UNDO_NAME = "load_undo.ppst";
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
// Used for "confirm exit if you haven't saved in a while"
|
||||
double g_lastSaveTime = -1.0;
|
||||
|
||||
struct SaveStart
|
||||
{
|
||||
void DoState(PointerWrap &p);
|
||||
};
|
||||
static bool needsProcess = false;
|
||||
static bool needsRestart = false;
|
||||
static std::mutex mutex;
|
||||
static int screenshotFailures = 0;
|
||||
static bool hasLoadedState = false;
|
||||
static const int STALE_STATE_USES = 2;
|
||||
// 4 hours of total gameplay since the virtual PSP started the game.
|
||||
static const u64 STALE_STATE_TIME = 4 * 3600 * 1000000ULL;
|
||||
static int saveStateGeneration = 0;
|
||||
static int saveDataGeneration = 0;
|
||||
static int lastSaveDataGeneration = 0;
|
||||
static std::string saveStateInitialGitVersion = "";
|
||||
static StateRingbuffer rewindStates;
|
||||
|
||||
enum OperationType
|
||||
{
|
||||
SAVESTATE_SAVE,
|
||||
SAVESTATE_LOAD,
|
||||
SAVESTATE_VERIFY,
|
||||
SAVESTATE_REWIND,
|
||||
SAVESTATE_SAVE_SCREENSHOT,
|
||||
};
|
||||
struct SaveStart {
|
||||
void DoState(PointerWrap &p);
|
||||
};
|
||||
|
||||
enum class OperationType {
|
||||
Save,
|
||||
Load,
|
||||
Verify,
|
||||
Rewind,
|
||||
SaveScreenshot,
|
||||
};
|
||||
|
||||
struct Operation {
|
||||
// The slot number is for visual purposes only. Set to -1 for operations where we don't display a message for example.
|
||||
@@ -89,6 +109,8 @@ double g_lastSaveTime = -1.0;
|
||||
int slot;
|
||||
};
|
||||
|
||||
static std::vector<Operation> pending;
|
||||
|
||||
CChunkFileReader::Error SaveToRam(std::vector<u8> &data) {
|
||||
SaveStart state;
|
||||
return CChunkFileReader::MeasureAndSavePtr(state, &data);
|
||||
@@ -99,248 +121,12 @@ double g_lastSaveTime = -1.0;
|
||||
return CChunkFileReader::LoadPtr(&data[0], state, errorString);
|
||||
}
|
||||
|
||||
// This ring buffer of states is for rewind save states, which are kept in RAM.
|
||||
// Save states are compressed against one of two reference saves (bases_), and the reference
|
||||
// is switched to a fresh save every N saves, where N is BASE_USAGE_INTERVAL.
|
||||
// The compression is a simple block based scheme where 0 means to copy a block from the base,
|
||||
// and 1 means that the following bytes are the next block. See Compress/LockedDecompress.
|
||||
class StateRingbuffer {
|
||||
public:
|
||||
StateRingbuffer() {
|
||||
size_ = REWIND_NUM_STATES;
|
||||
states_.resize(size_);
|
||||
baseMapping_.resize(size_);
|
||||
}
|
||||
|
||||
~StateRingbuffer() {
|
||||
if (compressThread_.joinable()) {
|
||||
compressThread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Save()
|
||||
{
|
||||
rewindLastTime_ = time_now_d();
|
||||
|
||||
// Make sure we're not processing a previous save. That'll cause a hitch though, but at least won't
|
||||
// crash due to contention over buffer_.
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
int n = next_++ % size_;
|
||||
if ((next_ % size_) == first_)
|
||||
++first_;
|
||||
|
||||
std::vector<u8> *compressBuffer = &buffer_;
|
||||
CChunkFileReader::Error err;
|
||||
|
||||
if (base_ == -1 || ++baseUsage_ > BASE_USAGE_INTERVAL)
|
||||
{
|
||||
base_ = (base_ + 1) % ARRAY_SIZE(bases_);
|
||||
baseUsage_ = 0;
|
||||
err = SaveToRam(bases_[base_]);
|
||||
// Let's not bother savestating twice.
|
||||
compressBuffer = &bases_[base_];
|
||||
}
|
||||
else
|
||||
err = SaveToRam(buffer_);
|
||||
|
||||
if (err == CChunkFileReader::ERROR_NONE)
|
||||
ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]);
|
||||
else
|
||||
states_[n].clear();
|
||||
|
||||
baseMapping_[n] = base_;
|
||||
return err;
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Restore(std::string *errorString)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
// No valid states left.
|
||||
if (Empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
int n = (--next_ + size_) % size_;
|
||||
if (states_[n].empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
static std::vector<u8> buffer;
|
||||
LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]);
|
||||
CChunkFileReader::Error error = LoadFromRam(buffer, errorString);
|
||||
rewindLastTime_ = time_now_d();
|
||||
return error;
|
||||
}
|
||||
|
||||
void ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base)
|
||||
{
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
compressThread_ = std::thread([=]{
|
||||
SetCurrentThreadName("SaveStateCompress");
|
||||
|
||||
// Should do no I/O, so no JNI thread context needed.
|
||||
Compress(*result, *state, *base);
|
||||
});
|
||||
}
|
||||
|
||||
void Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
// Bail if we were cleared before locking.
|
||||
if (first_ == 0 && next_ == 0)
|
||||
return;
|
||||
|
||||
double start_time = time_now_d();
|
||||
result.clear();
|
||||
result.reserve(512 * 1024);
|
||||
for (size_t i = 0; i < state.size(); i += BLOCK_SIZE)
|
||||
{
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(state.size() - i));
|
||||
if (i + blockSize > base.size() || memcmp(&state[i], &base[i], blockSize) != 0)
|
||||
{
|
||||
result.push_back(1);
|
||||
result.insert(result.end(), state.begin() + i, state.begin() + i + blockSize);
|
||||
}
|
||||
else
|
||||
result.push_back(0);
|
||||
}
|
||||
|
||||
double taken_s = time_now_d() - start_time;
|
||||
DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0);
|
||||
}
|
||||
|
||||
void LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base)
|
||||
{
|
||||
result.clear();
|
||||
result.reserve(base.size());
|
||||
auto basePos = base.begin();
|
||||
for (size_t i = 0; i < compressed.size(); )
|
||||
{
|
||||
if (compressed[i] == 0)
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(base.size() - result.size()));
|
||||
result.insert(result.end(), basePos, basePos + blockSize);
|
||||
basePos += blockSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i));
|
||||
result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize);
|
||||
i += blockSize;
|
||||
// This check is to avoid advancing basePos out of range, which MSVC catches.
|
||||
// When this happens, we're at the end of decoding anyway.
|
||||
if (base.end() - basePos >= blockSize) {
|
||||
basePos += blockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Clear()
|
||||
{
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
// This lock is mainly for shutdown.
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
first_ = 0;
|
||||
next_ = 0;
|
||||
for (auto &b : bases_) {
|
||||
b.clear();
|
||||
}
|
||||
baseMapping_.clear();
|
||||
baseMapping_.resize(size_);
|
||||
for (auto &s : states_) {
|
||||
s.clear();
|
||||
}
|
||||
buffer_.clear();
|
||||
base_ = -1;
|
||||
baseUsage_ = 0;
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
bool Empty() const
|
||||
{
|
||||
return next_ == first_;
|
||||
}
|
||||
|
||||
void Process() {
|
||||
if (g_Config.iRewindSnapshotInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
if (coreState != CORE_RUNNING_CPU) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For fast-forwarding, otherwise they may be useless and too close.
|
||||
double now = time_now_d();
|
||||
double diff = now - rewindLastTime_;
|
||||
if (diff < g_Config.iRewindSnapshotInterval)
|
||||
return;
|
||||
|
||||
DEBUG_LOG(Log::SaveState, "Saving rewind state");
|
||||
Save();
|
||||
}
|
||||
|
||||
void NotifyState() {
|
||||
// Prevent saving snapshots immediately after loading or saving a state.
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
private:
|
||||
const int BLOCK_SIZE = 8192;
|
||||
const int REWIND_NUM_STATES = 20;
|
||||
// TODO: Instead, based on size of compressed state?
|
||||
const int BASE_USAGE_INTERVAL = 15;
|
||||
|
||||
typedef std::vector<u8> StateBuffer;
|
||||
|
||||
int first_ = 0;
|
||||
int next_ = 0;
|
||||
int size_;
|
||||
|
||||
std::vector<StateBuffer> states_;
|
||||
StateBuffer bases_[2];
|
||||
std::vector<int> baseMapping_;
|
||||
std::mutex lock_;
|
||||
std::thread compressThread_;
|
||||
std::vector<u8> buffer_;
|
||||
|
||||
int base_ = -1;
|
||||
int baseUsage_ = 0;
|
||||
|
||||
double rewindLastTime_ = 0.0f;
|
||||
};
|
||||
|
||||
static bool needsProcess = false;
|
||||
static bool needsRestart = false;
|
||||
static std::vector<Operation> pending;
|
||||
static std::mutex mutex;
|
||||
static int screenshotFailures = 0;
|
||||
static bool hasLoadedState = false;
|
||||
static const int STALE_STATE_USES = 2;
|
||||
// 4 hours of total gameplay since the virtual PSP started the game.
|
||||
static const u64 STALE_STATE_TIME = 4 * 3600 * 1000000ULL;
|
||||
static int saveStateGeneration = 0;
|
||||
static int saveDataGeneration = 0;
|
||||
static int lastSaveDataGeneration = 0;
|
||||
static std::string saveStateInitialGitVersion = "";
|
||||
|
||||
// TODO: Should this be configurable?
|
||||
// Should only fail if the game hasn't created a framebuffer yet. Some games play video without a framebuffer,
|
||||
// we should probably just read back memory then instead (GTA for example). But this is a really unimportant edge case,
|
||||
// when in-game it's just not an issue.
|
||||
static const int SCREENSHOT_FAILURE_RETRIES = 6;
|
||||
static StateRingbuffer rewindStates;
|
||||
|
||||
void SaveStart::DoState(PointerWrap &p)
|
||||
{
|
||||
void SaveStart::DoState(PointerWrap &p) {
|
||||
auto s = p.Section("SaveStart", 1, 3);
|
||||
if (!s)
|
||||
return;
|
||||
@@ -404,13 +190,12 @@ double g_lastSaveTime = -1.0;
|
||||
pspFileSystem.DoState(p);
|
||||
}
|
||||
|
||||
void Enqueue(const SaveState::Operation &op)
|
||||
{
|
||||
void Enqueue(const SaveState::Operation &op) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return;
|
||||
}
|
||||
if (Achievements::HardcoreModeActive()) {
|
||||
if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::SAVESTATE_SAVE) || (op.type == SAVESTATE_SAVE_SCREENSHOT))) {
|
||||
if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::OperationType::Save) || (op.type == OperationType::SaveScreenshot))) {
|
||||
// We allow saving in hardcore mode if this setting is on.
|
||||
} else {
|
||||
// Operation not allowed
|
||||
@@ -434,7 +219,7 @@ double g_lastSaveTime = -1.0;
|
||||
rewindStates.NotifyState();
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateLoad, 0);
|
||||
Enqueue(Operation(SAVESTATE_LOAD, filename, slot, callback));
|
||||
Enqueue(Operation(OperationType::Load, filename, slot, callback));
|
||||
}
|
||||
|
||||
void Save(const Path &filename, int slot, Callback callback) {
|
||||
@@ -445,11 +230,11 @@ double g_lastSaveTime = -1.0;
|
||||
rewindStates.NotifyState();
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateSave, 0);
|
||||
Enqueue(Operation(SAVESTATE_SAVE, filename, slot, callback));
|
||||
Enqueue(Operation(OperationType::Save, filename, slot, callback));
|
||||
}
|
||||
|
||||
void Verify(Callback callback) {
|
||||
Enqueue(Operation(SAVESTATE_VERIFY, Path(), -1, callback));
|
||||
Enqueue(Operation(OperationType::Verify, Path(), -1, callback));
|
||||
}
|
||||
|
||||
void Rewind(Callback callback) {
|
||||
@@ -458,12 +243,12 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateRewind, 0);
|
||||
Enqueue(Operation(SAVESTATE_REWIND, Path(), -1, callback));
|
||||
Enqueue(Operation(OperationType::Rewind, Path(), -1, callback));
|
||||
}
|
||||
|
||||
static void SaveScreenshot(const Path &filename) {
|
||||
screenshotFailures = 0;
|
||||
Enqueue(Operation(SAVESTATE_SAVE_SCREENSHOT, filename, -1, nullptr));
|
||||
Enqueue(Operation(OperationType::SaveScreenshot, filename, -1, nullptr));
|
||||
}
|
||||
|
||||
bool CanRewind() {
|
||||
@@ -530,9 +315,13 @@ double g_lastSaveTime = -1.0;
|
||||
return filename.GetFilename() + " " + std::string(sy->T("(broken)"));
|
||||
}
|
||||
|
||||
std::string GenerateFullDiscId(const Path &gameFilename) {
|
||||
std::string discId = g_paramSFO.GetValueString("DISC_ID");
|
||||
std::string discVer = g_paramSFO.GetValueString("DISC_VERSION");
|
||||
// TODO: gameFilename is ignored!
|
||||
// This means that this must always be called in-game, otherwise it will not work!
|
||||
//
|
||||
// This particular ID string generation is specific for save states.
|
||||
static std::string GenerateFullDiscId(const ParamSFOData ¶mSFO) {
|
||||
std::string discId = paramSFO.GetValueString("DISC_ID");
|
||||
std::string discVer = paramSFO.GetValueString("DISC_VERSION");
|
||||
if (discId.empty()) {
|
||||
// Should never happen.
|
||||
discId = g_paramSFO.GenerateFakeID(Path());
|
||||
@@ -541,32 +330,31 @@ double g_lastSaveTime = -1.0;
|
||||
return StringFromFormat("%s_%s", discId.c_str(), discVer.c_str());
|
||||
}
|
||||
|
||||
Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension)
|
||||
{
|
||||
std::string filename = StringFromFormat("%s_%d.%s", GenerateFullDiscId(gameFilename).c_str(), slot, extension);
|
||||
std::string GetGamePrefix(const ParamSFOData ¶mSFO) {
|
||||
return GenerateFullDiscId(paramSFO);
|
||||
}
|
||||
|
||||
// The prefix is always based on GenerateFullDiscId. So we can find these by scanning, too.
|
||||
Path GenerateSaveSlotFilename(std::string_view gamePrefix, int slot, const char *extension) {
|
||||
std::string filename = StringFromFormat("%.*s_%d.%s", STR_VIEW(gamePrefix), slot, extension);
|
||||
return GetSysDirectory(DIRECTORY_SAVESTATE) / filename;
|
||||
}
|
||||
|
||||
int GetCurrentSlot()
|
||||
{
|
||||
int GetCurrentSlot() {
|
||||
return g_Config.iCurrentStateSlot;
|
||||
}
|
||||
|
||||
void PrevSlot()
|
||||
{
|
||||
void PrevSlot() {
|
||||
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot - 1 + Config::iSaveStateSlotCount) % Config::iSaveStateSlotCount;
|
||||
}
|
||||
|
||||
void NextSlot()
|
||||
{
|
||||
void NextSlot() {
|
||||
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot + 1) % Config::iSaveStateSlotCount;
|
||||
}
|
||||
|
||||
static void DeleteIfExists(const Path &fn) {
|
||||
// Just avoiding error messages.
|
||||
if (File::Exists(fn)) {
|
||||
File::Delete(fn);
|
||||
}
|
||||
File::Delete(fn, true);
|
||||
}
|
||||
|
||||
static void RenameIfExists(const Path &from, const Path &to) {
|
||||
@@ -584,13 +372,12 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
void LoadSlot(const Path &gameFilename, int slot, Callback callback)
|
||||
{
|
||||
void LoadSlot(std::string_view gamePrefix, int slot, Callback callback) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
if (!fn.empty()) {
|
||||
// This add only 1 extra state, should we just always enable it?
|
||||
if (g_Config.bEnableStateUndo) {
|
||||
@@ -600,7 +387,7 @@ double g_lastSaveTime = -1.0;
|
||||
if (status != Status::FAILURE) {
|
||||
DeleteIfExists(backup);
|
||||
File::Rename(backup.WithExtraExtension(".tmp"), backup);
|
||||
g_Config.sStateLoadUndoGame = GenerateFullDiscId(gameFilename);
|
||||
g_Config.sStateLoadUndoGame = gamePrefix;
|
||||
g_Config.Save("Saving config for savestate last load undo");
|
||||
} else {
|
||||
ERROR_LOG(Log::SaveState, "Saving load undo state failed: %.*s", (int)message.size(), message.data());
|
||||
@@ -625,12 +412,12 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
bool UndoLoad(const Path &gameFilename, Callback callback) {
|
||||
bool UndoLoad(std::string_view gamePrefix, Callback callback) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_Config.sStateLoadUndoGame != GenerateFullDiscId(gameFilename)) {
|
||||
if (g_Config.sStateLoadUndoGame != gamePrefix) {
|
||||
if (callback) {
|
||||
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
||||
callback(Status::FAILURE, sy->T("Error: load undo state is from a different game"));
|
||||
@@ -651,21 +438,21 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
void SaveSlot(const Path &gameFilename, int slot, Callback callback) {
|
||||
void SaveSlot(std::string_view gamePrefix, int slot, Callback callback) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
Path fnUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION);
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
Path fnUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION);
|
||||
if (!fn.empty()) {
|
||||
Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
||||
Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION);
|
||||
auto renameCallback = [=](Status status, std::string_view message) {
|
||||
if (status != Status::FAILURE) {
|
||||
if (g_Config.bEnableStateUndo) {
|
||||
DeleteIfExists(fnUndo);
|
||||
RenameIfExists(fn, fnUndo);
|
||||
g_Config.sStateUndoLastSaveGame = GenerateFullDiscId(gameFilename);
|
||||
g_Config.sStateUndoLastSaveGame = gamePrefix;
|
||||
g_Config.iStateUndoLastSaveSlot = slot;
|
||||
g_Config.Save("Saving config for savestate last save undo");
|
||||
} else {
|
||||
@@ -679,7 +466,7 @@ double g_lastSaveTime = -1.0;
|
||||
};
|
||||
// Let's also create a screenshot.
|
||||
if (g_Config.bEnableStateUndo) {
|
||||
Path shotUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_SCREENSHOT_EXTENSION);
|
||||
Path shotUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION);
|
||||
DeleteIfExists(shotUndo);
|
||||
RenameIfExists(shot, shotUndo);
|
||||
}
|
||||
@@ -693,18 +480,18 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
bool UndoSaveSlot(const Path &gameFilename, int slot) {
|
||||
bool UndoSaveSlot(std::string_view gamePrefix, int slot) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Path fnUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION);
|
||||
Path fnUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION);
|
||||
|
||||
// Do nothing if there's no undo.
|
||||
if (File::Exists(fnUndo)) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
||||
Path shotUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_SCREENSHOT_EXTENSION);
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION);
|
||||
Path shotUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION);
|
||||
// Swap them so they can undo again to redo. Mistakes happen.
|
||||
SwapIfExists(shotUndo, shot);
|
||||
SwapIfExists(fnUndo, fn);
|
||||
@@ -714,9 +501,9 @@ double g_lastSaveTime = -1.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void DeleteSlot(const Path &gameFilename, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
||||
void DeleteSlot(std::string_view gamePrefix, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION);
|
||||
|
||||
if (File::Exists(fn)) {
|
||||
DeleteIfExists(fn);
|
||||
@@ -724,47 +511,57 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
bool UndoLastSave(const Path &gameFilename) {
|
||||
bool UndoLastSave(std::string_view gamePrefix) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_Config.sStateUndoLastSaveGame != GenerateFullDiscId(gameFilename))
|
||||
if (g_Config.sStateUndoLastSaveGame != gamePrefix)
|
||||
return false;
|
||||
|
||||
return UndoSaveSlot(gameFilename, g_Config.iStateUndoLastSaveSlot);
|
||||
return UndoSaveSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot);
|
||||
}
|
||||
|
||||
bool HasSaveInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
void Rescan(std::string_view gamePrefix) {
|
||||
// Currently nothing to do here.
|
||||
// On Android this would be important to rescan the save state directory.
|
||||
std::vector<File::FileInfo> file;
|
||||
|
||||
std::string prefix(gamePrefix);
|
||||
prefix += "_";
|
||||
|
||||
File::GetFilesInDir(GetSysDirectory(DIRECTORY_SAVESTATE), &file, nullptr, 0, prefix);
|
||||
|
||||
for (const auto &f : file) {
|
||||
INFO_LOG(Log::System, "%s", f.name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool HasSaveInSlot(std::string_view gamePrefix, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoSaveInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION);
|
||||
bool HasUndoSaveInSlot(std::string_view gamePrefix, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoLastSave(const Path &gameFilename)
|
||||
{
|
||||
if (g_Config.sStateUndoLastSaveGame != GenerateFullDiscId(gameFilename))
|
||||
bool HasUndoLastSave(std::string_view gamePrefix) {
|
||||
if (g_Config.sStateUndoLastSaveGame != gamePrefix)
|
||||
return false;
|
||||
|
||||
return HasUndoSaveInSlot(gameFilename, g_Config.iStateUndoLastSaveSlot);
|
||||
return HasUndoSaveInSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot);
|
||||
}
|
||||
|
||||
bool HasScreenshotInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
||||
bool HasScreenshotInSlot(std::string_view gamePrefix, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoLoad(const Path &gameFilename)
|
||||
{
|
||||
bool HasUndoLoad(std::string_view gamePrefix) {
|
||||
Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME;
|
||||
return File::Exists(fn) && g_Config.sStateLoadUndoGame == GenerateFullDiscId(gameFilename);
|
||||
return File::Exists(fn) && g_Config.sStateLoadUndoGame == gamePrefix;
|
||||
}
|
||||
|
||||
bool operator < (const tm &t1, const tm &t2) {
|
||||
@@ -804,11 +601,11 @@ double g_lastSaveTime = -1.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
int GetNewestSlot(const Path &gameFilename) {
|
||||
int GetNewestSlot(std::string_view gamePrefix) {
|
||||
int newestSlot = -1;
|
||||
tm newestDate = {0};
|
||||
for (int i = 0; i < Config::iSaveStateSlotCount; i++) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, i, STATE_EXTENSION);
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION);
|
||||
if (File::Exists(fn)) {
|
||||
tm time;
|
||||
bool success = File::GetModifTime(fn, time);
|
||||
@@ -821,11 +618,11 @@ double g_lastSaveTime = -1.0;
|
||||
return newestSlot;
|
||||
}
|
||||
|
||||
int GetOldestSlot(const Path &gameFilename) {
|
||||
int GetOldestSlot(std::string_view gamePrefix) {
|
||||
int oldestSlot = -1;
|
||||
tm oldestDate = {0};
|
||||
for (int i = 0; i < Config::iSaveStateSlotCount; i++) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, i, STATE_EXTENSION);
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION);
|
||||
if (File::Exists(fn)) {
|
||||
tm time;
|
||||
bool success = File::GetModifTime(fn, time);
|
||||
@@ -838,8 +635,8 @@ double g_lastSaveTime = -1.0;
|
||||
return oldestSlot;
|
||||
}
|
||||
|
||||
std::string GetSlotDateAsString(const Path &gameFilename, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
|
||||
tm time;
|
||||
if (File::GetModifTime(fn, time)) {
|
||||
char buf[256];
|
||||
@@ -862,8 +659,7 @@ double g_lastSaveTime = -1.0;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<Operation> Flush()
|
||||
{
|
||||
std::vector<Operation> Flush() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
std::vector<Operation> copy = pending;
|
||||
pending.clear();
|
||||
@@ -950,6 +746,8 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
|
||||
// NOTE: This can cause ending of the current renderpass, due to the readback needed for the screenshot.
|
||||
// TODO: This should run the actual operations on a thread. While this returns true (for example), emulation
|
||||
// *must* not run further, in order not to disturb the current state operation.
|
||||
bool Process() {
|
||||
rewindStates.Process();
|
||||
|
||||
@@ -957,8 +755,7 @@ double g_lastSaveTime = -1.0;
|
||||
return false;
|
||||
needsProcess = false;
|
||||
|
||||
if (!__KernelIsRunning())
|
||||
{
|
||||
if (!__KernelIsRunning()) {
|
||||
ERROR_LOG(Log::SaveState, "Savestate failure: Unable to load without kernel, this should never happen.");
|
||||
return false;
|
||||
}
|
||||
@@ -981,9 +778,8 @@ double g_lastSaveTime = -1.0;
|
||||
std::string slot_prefix = op.slot >= 0 ? StringFromFormat("(%d) ", op.slot + 1) : "";
|
||||
std::string errorString;
|
||||
|
||||
switch (op.type)
|
||||
{
|
||||
case SAVESTATE_LOAD:
|
||||
switch (op.type) {
|
||||
case OperationType::Load:
|
||||
INFO_LOG(Log::SaveState, "Loading state from '%s'", op.filename.c_str());
|
||||
// Use the state's latest version as a guess for saveStateInitialGitVersion.
|
||||
result = CChunkFileReader::Load(op.filename, &saveStateInitialGitVersion, state, &errorString);
|
||||
@@ -1019,7 +815,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_SAVE:
|
||||
case OperationType::Save:
|
||||
INFO_LOG(Log::SaveState, "Saving state to '%s'", op.filename.c_str());
|
||||
title = g_paramSFO.GetValueString("TITLE");
|
||||
if (title.empty()) {
|
||||
@@ -1055,7 +851,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_VERIFY:
|
||||
case OperationType::Verify:
|
||||
{
|
||||
int tempResult = CChunkFileReader::Verify(state) == CChunkFileReader::ERROR_NONE;
|
||||
callbackResult = tempResult ? Status::SUCCESS : Status::FAILURE;
|
||||
@@ -1067,7 +863,7 @@ double g_lastSaveTime = -1.0;
|
||||
break;
|
||||
}
|
||||
|
||||
case SAVESTATE_REWIND:
|
||||
case OperationType::Rewind:
|
||||
INFO_LOG(Log::SaveState, "Rewinding to recent savestate snapshot");
|
||||
result = rewindStates.Restore(&errorString);
|
||||
if (result == CChunkFileReader::ERROR_NONE) {
|
||||
@@ -1093,7 +889,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_SAVE_SCREENSHOT:
|
||||
case OperationType::SaveScreenshot:
|
||||
{
|
||||
_dbg_assert_(!op.callback);
|
||||
|
||||
@@ -1158,8 +954,7 @@ double g_lastSaveTime = -1.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
void Init() {
|
||||
// Make sure there's a directory for save slots
|
||||
File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVESTATE));
|
||||
|
||||
@@ -1175,8 +970,7 @@ double g_lastSaveTime = -1.0;
|
||||
g_lastSaveTime = time_now_d();
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
void Shutdown() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
rewindStates.Clear();
|
||||
}
|
||||
|
||||
+27
-21
@@ -25,6 +25,9 @@
|
||||
#include "Common/File/Path.h"
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
|
||||
class ParamSFOData;
|
||||
#undef Process
|
||||
|
||||
namespace SaveState {
|
||||
enum class Status {
|
||||
FAILURE,
|
||||
@@ -33,12 +36,7 @@ namespace SaveState {
|
||||
};
|
||||
typedef std::function<void(Status status, std::string_view message)> Callback;
|
||||
|
||||
static const char * const STATE_EXTENSION = "ppst";
|
||||
static const char * const SCREENSHOT_EXTENSION = "jpg";
|
||||
static const char * const UNDO_STATE_EXTENSION = "undo.ppst";
|
||||
static const char * const UNDO_SCREENSHOT_EXTENSION = "undo.jpg";
|
||||
|
||||
static const char * const LOAD_UNDO_NAME = "load_undo.ppst";
|
||||
|
||||
void Init();
|
||||
void Shutdown();
|
||||
@@ -46,29 +44,37 @@ namespace SaveState {
|
||||
// Cycle through the 5 savestate slots
|
||||
void PrevSlot();
|
||||
void NextSlot();
|
||||
void SaveSlot(const Path &gameFilename, int slot, Callback callback);
|
||||
void LoadSlot(const Path &gameFilename, int slot, Callback callback);
|
||||
bool UndoSaveSlot(const Path &gameFilename, int slot);
|
||||
bool UndoLastSave(const Path &gameFilename);
|
||||
bool UndoLoad(const Path &gameFilename, Callback callback);
|
||||
void DeleteSlot(const Path &gameFilename, int slot);
|
||||
|
||||
std::string GetGamePrefix(const ParamSFOData ¶mSFO);
|
||||
|
||||
// Run the various actions directly.
|
||||
void SaveSlot(std::string_view gamePrefix, int slot, Callback callback);
|
||||
void LoadSlot(std::string_view gamePrefix, int slot, Callback callback);
|
||||
bool UndoSaveSlot(std::string_view gamePrefix, int slot);
|
||||
bool UndoLastSave(std::string_view gamePrefix);
|
||||
bool UndoLoad(std::string_view gamePrefix, Callback callback);
|
||||
void DeleteSlot(std::string_view gamePrefix, int slot);
|
||||
|
||||
// This will rescan the save state directory for files associated with the specified game.
|
||||
// Note that when we have many save states, this is really important on Android.
|
||||
void Rescan(std::string_view gamePrefix);
|
||||
|
||||
// Checks whether there's an existing save in the specified slot.
|
||||
bool HasSaveInSlot(const Path &gameFilename, int slot);
|
||||
bool HasUndoSaveInSlot(const Path &gameFilename, int slot);
|
||||
bool HasUndoLastSave(const Path &gameFilename);
|
||||
bool HasUndoLoad(const Path &gameFilename);
|
||||
bool HasScreenshotInSlot(const Path &gameFilename, int slot);
|
||||
bool HasSaveInSlot(std::string_view gamePrefix, int slot);
|
||||
bool HasUndoSaveInSlot(std::string_view gamePrefix, int slot);
|
||||
bool HasUndoLastSave(std::string_view gamePrefix);
|
||||
bool HasUndoLoad(std::string_view gamePrefix);
|
||||
bool HasScreenshotInSlot(std::string_view gamePrefix, int slot);
|
||||
|
||||
// Just returns the current slot from config.
|
||||
int GetCurrentSlot();
|
||||
|
||||
// Returns -1 if there's no oldest/newest slot.
|
||||
int GetNewestSlot(const Path &gameFilename);
|
||||
int GetOldestSlot(const Path &gameFilename);
|
||||
int GetNewestSlot(std::string_view gamePrefix);
|
||||
int GetOldestSlot(std::string_view gamePrefix);
|
||||
|
||||
std::string GetSlotDateAsString(const Path &gameFilename, int slot);
|
||||
std::string GenerateFullDiscId(const Path &gameFilename);
|
||||
Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension);
|
||||
std::string GetSlotDateAsString(std::string_view gamePrefix, int slot);
|
||||
Path GenerateSaveSlotFilename(std::string_view gamePrefix, int slot, const char *extension);
|
||||
|
||||
std::string GetTitle(const Path &filename);
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "Common/Thread/ThreadUtil.h"
|
||||
#include "Core/SaveState.h"
|
||||
#include "Core/SaveStateRewind.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Config.h"
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
CChunkFileReader::Error StateRingbuffer::Save() {
|
||||
rewindLastTime_ = time_now_d();
|
||||
|
||||
// Make sure we're not processing a previous save. That'll cause a hitch though, but at least won't
|
||||
// crash due to contention over buffer_.
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
int n = next_++ % size_;
|
||||
if ((next_ % size_) == first_)
|
||||
++first_;
|
||||
|
||||
std::vector<u8> *compressBuffer = &buffer_;
|
||||
CChunkFileReader::Error err;
|
||||
|
||||
if (base_ == -1 || ++baseUsage_ > BASE_USAGE_INTERVAL)
|
||||
{
|
||||
base_ = (base_ + 1) % ARRAY_SIZE(bases_);
|
||||
baseUsage_ = 0;
|
||||
err = SaveToRam(bases_[base_]);
|
||||
// Let's not bother savestating twice.
|
||||
compressBuffer = &bases_[base_];
|
||||
} else
|
||||
err = SaveToRam(buffer_);
|
||||
|
||||
if (err == CChunkFileReader::ERROR_NONE)
|
||||
ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]);
|
||||
else
|
||||
states_[n].clear();
|
||||
|
||||
baseMapping_[n] = base_;
|
||||
return err;
|
||||
}
|
||||
|
||||
CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
// No valid states left.
|
||||
if (Empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
int n = (--next_ + size_) % size_;
|
||||
if (states_[n].empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
static std::vector<u8> buffer;
|
||||
LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]);
|
||||
CChunkFileReader::Error error = LoadFromRam(buffer, errorString);
|
||||
rewindLastTime_ = time_now_d();
|
||||
return error;
|
||||
}
|
||||
|
||||
void StateRingbuffer::ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base) {
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
compressThread_ = std::thread([=] {
|
||||
SetCurrentThreadName("SaveStateCompress");
|
||||
|
||||
// Should do no I/O, so no JNI thread context needed.
|
||||
Compress(*result, *state, *base);
|
||||
});
|
||||
}
|
||||
|
||||
void StateRingbuffer::Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
// Bail if we were cleared before locking.
|
||||
if (first_ == 0 && next_ == 0)
|
||||
return;
|
||||
|
||||
double start_time = time_now_d();
|
||||
result.clear();
|
||||
result.reserve(512 * 1024);
|
||||
for (size_t i = 0; i < state.size(); i += BLOCK_SIZE)
|
||||
{
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(state.size() - i));
|
||||
if (i + blockSize > base.size() || memcmp(&state[i], &base[i], blockSize) != 0) {
|
||||
result.push_back(1);
|
||||
result.insert(result.end(), state.begin() + i, state.begin() + i + blockSize);
|
||||
} else {
|
||||
result.push_back(0);
|
||||
}
|
||||
}
|
||||
|
||||
double taken_s = time_now_d() - start_time;
|
||||
DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0);
|
||||
}
|
||||
|
||||
void StateRingbuffer::LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base) {
|
||||
result.clear();
|
||||
result.reserve(base.size());
|
||||
auto basePos = base.begin();
|
||||
for (size_t i = 0; i < compressed.size(); )
|
||||
{
|
||||
if (compressed[i] == 0)
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(base.size() - result.size()));
|
||||
result.insert(result.end(), basePos, basePos + blockSize);
|
||||
basePos += blockSize;
|
||||
} else
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i));
|
||||
result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize);
|
||||
i += blockSize;
|
||||
// This check is to avoid advancing basePos out of range, which MSVC catches.
|
||||
// When this happens, we're at the end of decoding anyway.
|
||||
if (base.end() - basePos >= blockSize) {
|
||||
basePos += blockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StateRingbuffer::Clear() {
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
// This lock is mainly for shutdown.
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
first_ = 0;
|
||||
next_ = 0;
|
||||
for (auto &b : bases_) {
|
||||
b.clear();
|
||||
}
|
||||
baseMapping_.clear();
|
||||
baseMapping_.resize(size_);
|
||||
for (auto &s : states_) {
|
||||
s.clear();
|
||||
}
|
||||
buffer_.clear();
|
||||
base_ = -1;
|
||||
baseUsage_ = 0;
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
void StateRingbuffer::Process() {
|
||||
if (g_Config.iRewindSnapshotInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
if (coreState != CORE_RUNNING_CPU) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For fast-forwarding, otherwise they may be useless and too close.
|
||||
double now = time_now_d();
|
||||
double diff = now - rewindLastTime_;
|
||||
if (diff < g_Config.iRewindSnapshotInterval)
|
||||
return;
|
||||
|
||||
DEBUG_LOG(Log::SaveState, "Saving rewind state");
|
||||
Save();
|
||||
}
|
||||
|
||||
void StateRingbuffer::NotifyState() {
|
||||
// Prevent saving snapshots immediately after loading or saving a state.
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
} // namespace SaveState
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
// This ring buffer of states is for rewind save states, which are kept in RAM.
|
||||
// Save states are compressed against one of two reference saves (bases_), and the reference
|
||||
// is switched to a fresh save every N saves, where N is BASE_USAGE_INTERVAL.
|
||||
// The compression is a simple block based scheme where 0 means to copy a block from the base,
|
||||
// and 1 means that the following bytes are the next block. See Compress/LockedDecompress.
|
||||
class StateRingbuffer {
|
||||
public:
|
||||
StateRingbuffer() {
|
||||
size_ = REWIND_NUM_STATES;
|
||||
states_.resize(size_);
|
||||
baseMapping_.resize(size_);
|
||||
}
|
||||
|
||||
~StateRingbuffer() {
|
||||
if (compressThread_.joinable()) {
|
||||
compressThread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Save();
|
||||
CChunkFileReader::Error Restore(std::string *errorString);
|
||||
void ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base);
|
||||
void Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base);
|
||||
void LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base);
|
||||
void Clear();
|
||||
|
||||
bool Empty() const {
|
||||
return next_ == first_;
|
||||
}
|
||||
|
||||
void Process();
|
||||
void NotifyState();
|
||||
|
||||
private:
|
||||
const int BLOCK_SIZE = 8192;
|
||||
const int REWIND_NUM_STATES = 20;
|
||||
// TODO: Instead, based on size of compressed state?
|
||||
const int BASE_USAGE_INTERVAL = 15;
|
||||
|
||||
typedef std::vector<u8> StateBuffer;
|
||||
|
||||
int first_ = 0;
|
||||
int next_ = 0;
|
||||
int size_;
|
||||
|
||||
std::vector<StateBuffer> states_;
|
||||
StateBuffer bases_[2];
|
||||
std::vector<int> baseMapping_;
|
||||
std::mutex lock_;
|
||||
std::thread compressThread_;
|
||||
std::vector<u8> buffer_;
|
||||
|
||||
int base_ = -1;
|
||||
int baseUsage_ = 0;
|
||||
|
||||
double rewindLastTime_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace SaveState
|
||||
+2
-2
@@ -168,13 +168,13 @@ static void SaveStateActionFinished(SaveState::Status status, std::string_view m
|
||||
void MainWindow::qlstateAct()
|
||||
{
|
||||
Path gamePath = PSP_CoreParameter().fileToStart;
|
||||
SaveState::LoadSlot(gamePath, 0, SaveStateActionFinished);
|
||||
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), 0, SaveStateActionFinished);
|
||||
}
|
||||
|
||||
void MainWindow::qsstateAct()
|
||||
{
|
||||
Path gamePath = PSP_CoreParameter().fileToStart;
|
||||
SaveState::SaveSlot(gamePath, 0, SaveStateActionFinished);
|
||||
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), 0, SaveStateActionFinished);
|
||||
}
|
||||
|
||||
void MainWindow::lstateAct()
|
||||
|
||||
+12
-8
@@ -821,12 +821,12 @@ void EmuScreen::onVKey(VirtKey virtualKeyCode, bool down) {
|
||||
|
||||
case VIRTKEY_SAVE_STATE:
|
||||
if (down && !Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
||||
SaveState::SaveSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
||||
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
||||
}
|
||||
break;
|
||||
case VIRTKEY_LOAD_STATE:
|
||||
if (down && !Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) {
|
||||
SaveState::LoadSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
||||
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction);
|
||||
}
|
||||
break;
|
||||
case VIRTKEY_PREVIOUS_SLOT:
|
||||
@@ -1481,9 +1481,11 @@ void EmuScreen::update() {
|
||||
if (saveStateSlot_ != currentSlot) {
|
||||
saveStateSlot_ = currentSlot;
|
||||
|
||||
const std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO);
|
||||
|
||||
Path fn;
|
||||
if (SaveState::HasSaveInSlot(gamePath_, currentSlot)) {
|
||||
fn = SaveState::GenerateSaveSlotFilename(gamePath_, currentSlot, SaveState::SCREENSHOT_EXTENSION);
|
||||
if (SaveState::HasSaveInSlot(gamePrefix, currentSlot)) {
|
||||
fn = SaveState::GenerateSaveSlotFilename(gamePrefix, currentSlot, SaveState::SCREENSHOT_EXTENSION);
|
||||
}
|
||||
|
||||
saveStatePreview_->SetFilename(fn);
|
||||
@@ -2035,23 +2037,25 @@ void EmuScreen::AutoLoadSaveState() {
|
||||
|
||||
int autoSlot = -1;
|
||||
|
||||
std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO);
|
||||
|
||||
//check if save state has save, if so, load
|
||||
switch (g_Config.iAutoLoadSaveState) {
|
||||
case (int)AutoLoadSaveState::OFF: // "AutoLoad Off"
|
||||
return;
|
||||
case (int)AutoLoadSaveState::OLDEST: // "Oldest Save"
|
||||
autoSlot = SaveState::GetOldestSlot(gamePath_);
|
||||
autoSlot = SaveState::GetOldestSlot(gamePrefix);
|
||||
break;
|
||||
case (int)AutoLoadSaveState::NEWEST: // "Newest Save"
|
||||
autoSlot = SaveState::GetNewestSlot(gamePath_);
|
||||
autoSlot = SaveState::GetNewestSlot(gamePrefix);
|
||||
break;
|
||||
default: // try the specific save state slot specified
|
||||
autoSlot = (SaveState::HasSaveInSlot(gamePath_, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1;
|
||||
autoSlot = (SaveState::HasSaveInSlot(gamePrefix, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (g_Config.iAutoLoadSaveState && autoSlot != -1) {
|
||||
SaveState::LoadSlot(gamePath_, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) {
|
||||
SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) {
|
||||
AfterSaveStateAction(status, message);
|
||||
auto sy = GetI18NCategory(I18NCat::SYSTEM);
|
||||
std::string msg = std::string(sy->T("Auto Load Savestate")) + ": " + StringFromFormat("%d", autoSlot);
|
||||
|
||||
+52
-40
@@ -78,8 +78,8 @@ static void AfterSaveStateAction(SaveState::Status status, std::string_view mess
|
||||
|
||||
class ScreenshotViewScreen : public UI::PopupScreen {
|
||||
public:
|
||||
ScreenshotViewScreen(const Path &filename, std::string title, int slot, Path gamePath)
|
||||
: PopupScreen(title), filename_(filename), slot_(slot), gamePath_(gamePath), title_(title) {} // PopupScreen will translate Back on its own
|
||||
ScreenshotViewScreen(const Path &screenshotFilename, std::string_view saveStatePrefix, std::string_view title, int slot, Path gamePath)
|
||||
: PopupScreen(title), screenshotFilename_(screenshotFilename), saveStatePrefix_(saveStatePrefix), slot_(slot), gamePath_(gamePath), title_(title) {} // PopupScreen will translate Back on its own
|
||||
|
||||
int GetSlot() const {
|
||||
return slot_;
|
||||
@@ -100,7 +100,7 @@ protected:
|
||||
ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
|
||||
LinearLayout *content = new LinearLayout(ORIENT_VERTICAL);
|
||||
Margins contentMargins(10, 0);
|
||||
content->Add(new AsyncImageFileView(filename_, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, contentMargins)))->SetCanBeFocused(false);
|
||||
content->Add(new AsyncImageFileView(screenshotFilename_, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, contentMargins)))->SetCanBeFocused(false);
|
||||
|
||||
GridLayoutSettings gridsettings(240, 64, 5);
|
||||
gridsettings.fillCells = true;
|
||||
@@ -108,7 +108,7 @@ protected:
|
||||
|
||||
Choice *back = new Choice(di->T("Back"));
|
||||
|
||||
const bool hasUndo = SaveState::HasUndoSaveInSlot(gamePath_, slot_);
|
||||
const bool hasUndo = SaveState::HasUndoSaveInSlot(saveStatePrefix_, slot_);
|
||||
const bool undoEnabled = g_Config.bEnableStateUndo;
|
||||
|
||||
Choice *undoButton = nullptr;
|
||||
@@ -138,8 +138,9 @@ private:
|
||||
void OnUndoState(UI::EventParams &e);
|
||||
void OnDeleteState(UI::EventParams &e);
|
||||
|
||||
Path filename_;
|
||||
Path screenshotFilename_;
|
||||
Path gamePath_;
|
||||
std::string saveStatePrefix_;
|
||||
std::string title_;
|
||||
int slot_;
|
||||
};
|
||||
@@ -147,7 +148,7 @@ private:
|
||||
void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) {
|
||||
if (!NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
g_Config.iCurrentStateSlot = slot_;
|
||||
SaveState::SaveSlot(gamePath_, slot_, &AfterSaveStateAction);
|
||||
SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction);
|
||||
TriggerFinish(DR_OK); //OK will close the pause screen as well
|
||||
}
|
||||
}
|
||||
@@ -155,14 +156,14 @@ void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) {
|
||||
void ScreenshotViewScreen::OnLoadState(UI::EventParams &e) {
|
||||
if (!NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
g_Config.iCurrentStateSlot = slot_;
|
||||
SaveState::LoadSlot(gamePath_, slot_, &AfterSaveStateAction);
|
||||
SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction);
|
||||
TriggerFinish(DR_OK);
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenshotViewScreen::OnUndoState(UI::EventParams &e) {
|
||||
if (!NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
SaveState::UndoSaveSlot(gamePath_, slot_);
|
||||
SaveState::UndoSaveSlot(saveStatePrefix_, slot_);
|
||||
TriggerFinish(DR_CANCEL);
|
||||
}
|
||||
}
|
||||
@@ -180,7 +181,7 @@ void ScreenshotViewScreen::OnDeleteState(UI::EventParams &e) {
|
||||
|
||||
screenManager()->push(new UI::MessagePopupScreen(title, message, di->T("Delete"), di->T("Cancel"), [=](bool result) {
|
||||
if (result) {
|
||||
SaveState::DeleteSlot(gamePath_, slot_);
|
||||
SaveState::DeleteSlot(saveStatePrefix_, slot_);
|
||||
TriggerFinish(DR_CANCEL);
|
||||
}
|
||||
}));
|
||||
@@ -188,7 +189,7 @@ void ScreenshotViewScreen::OnDeleteState(UI::EventParams &e) {
|
||||
|
||||
class SaveSlotView : public UI::LinearLayout {
|
||||
public:
|
||||
SaveSlotView(const Path &gamePath, int slot, UI::LayoutParams *layoutParams = nullptr);
|
||||
SaveSlotView(std::string_view saveStatePrefix, int slot, UI::LayoutParams *layoutParams = nullptr);
|
||||
|
||||
void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
|
||||
w = 500; h = 90;
|
||||
@@ -205,12 +206,13 @@ public:
|
||||
}
|
||||
|
||||
std::string GetScreenshotTitle() const {
|
||||
return SaveState::GetSlotDateAsString(gamePath_, slot_);
|
||||
return SaveState::GetSlotDateAsString(saveStatePrefix_, slot_);
|
||||
}
|
||||
|
||||
UI::Event OnStateLoaded;
|
||||
UI::Event OnStateSaved;
|
||||
UI::Event OnScreenshotClicked;
|
||||
UI::Event OnSelected;
|
||||
|
||||
private:
|
||||
void OnSaveState(UI::EventParams &e);
|
||||
@@ -220,19 +222,25 @@ private:
|
||||
UI::Button *loadStateButton_ = nullptr;
|
||||
|
||||
int slot_;
|
||||
Path gamePath_;
|
||||
std::string saveStatePrefix_;
|
||||
Path screenshotFilename_;
|
||||
};
|
||||
|
||||
SaveSlotView::SaveSlotView(const Path &gameFilename, int slot, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), slot_(slot), gamePath_(gameFilename) {
|
||||
SaveSlotView::SaveSlotView(std::string_view saveStatePrefix, int slot, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), slot_(slot), saveStatePrefix_(saveStatePrefix) {
|
||||
using namespace UI;
|
||||
|
||||
screenshotFilename_ = SaveState::GenerateSaveSlotFilename(gamePath_, slot, SaveState::SCREENSHOT_EXTENSION);
|
||||
screenshotFilename_ = SaveState::GenerateSaveSlotFilename(saveStatePrefix_, slot, SaveState::SCREENSHOT_EXTENSION);
|
||||
|
||||
std::string number = StringFromFormat("%d", slot + 1);
|
||||
Add(new Spacer(5));
|
||||
|
||||
Add(new TextView(number, new LinearLayoutParams(40.0f, WRAP_CONTENT, 0.0f, Gravity::G_VCENTER)))->SetBig(true);
|
||||
// TEMP HACK: use some other view, like a Choice, themed differently to enable keyboard access to selection.
|
||||
ClickableTextView *numberView = Add(new ClickableTextView(number, new LinearLayoutParams(40.0f, WRAP_CONTENT, 0.0f, Gravity::G_VCENTER)));
|
||||
numberView->SetBig(true);
|
||||
numberView->OnClick.Add([this](UI::EventParams &e) {
|
||||
e.v = this;
|
||||
OnSelected.Trigger(e);
|
||||
});
|
||||
|
||||
AsyncImageFileView *fv = Add(new AsyncImageFileView(screenshotFilename_, IS_DEFAULT, new UI::LayoutParams(82 * 2, 47 * 2)));
|
||||
|
||||
@@ -256,13 +264,13 @@ SaveSlotView::SaveSlotView(const Path &gameFilename, int slot, UI::LayoutParams
|
||||
OnScreenshotClicked.Trigger(e);
|
||||
});
|
||||
|
||||
if (SaveState::HasSaveInSlot(gamePath_, slot)) {
|
||||
if (SaveState::HasSaveInSlot(saveStatePrefix_, slot)) {
|
||||
if (!Achievements::HardcoreModeActive()) {
|
||||
loadStateButton_ = buttons->Add(new Button(pa->T("Load State"), new LinearLayoutParams(0.0, Gravity::G_VCENTER)));
|
||||
loadStateButton_->OnClick.Handle(this, &SaveSlotView::OnLoadState);
|
||||
}
|
||||
|
||||
std::string dateStr = SaveState::GetSlotDateAsString(gamePath_, slot_);
|
||||
std::string dateStr = SaveState::GetSlotDateAsString(saveStatePrefix_, slot_);
|
||||
if (!dateStr.empty()) {
|
||||
TextView *dateView = new TextView(dateStr, new LinearLayoutParams(0.0, Gravity::G_VCENTER));
|
||||
dateView->SetSmall(true);
|
||||
@@ -284,7 +292,7 @@ void SaveSlotView::Draw(UIContext &dc) {
|
||||
void SaveSlotView::OnLoadState(UI::EventParams &e) {
|
||||
if (!NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
g_Config.iCurrentStateSlot = slot_;
|
||||
SaveState::LoadSlot(gamePath_, slot_, &AfterSaveStateAction);
|
||||
SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction);
|
||||
UI::EventParams e2{};
|
||||
e2.v = this;
|
||||
OnStateLoaded.Trigger(e2);
|
||||
@@ -294,7 +302,7 @@ void SaveSlotView::OnLoadState(UI::EventParams &e) {
|
||||
void SaveSlotView::OnSaveState(UI::EventParams &e) {
|
||||
if (!NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
g_Config.iCurrentStateSlot = slot_;
|
||||
SaveState::SaveSlot(gamePath_, slot_, &AfterSaveStateAction);
|
||||
SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction);
|
||||
UI::EventParams e2{};
|
||||
e2.v = this;
|
||||
OnStateSaved.Trigger(e2);
|
||||
@@ -335,6 +343,7 @@ GamePauseScreen::GamePauseScreen(const Path &filename, bool bootPending)
|
||||
// So we can tell if something blew up while on the pause screen.
|
||||
std::string assertStr = "PauseScreen: " + filename.GetFilename();
|
||||
SetExtraAssertInfo(assertStr.c_str());
|
||||
saveStatePrefix_ = SaveState::GetGamePrefix(g_paramSFO);
|
||||
}
|
||||
|
||||
GamePauseScreen::~GamePauseScreen() {
|
||||
@@ -366,26 +375,41 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems)
|
||||
|
||||
leftColumnItems->SetSpacing(10.0);
|
||||
for (int i = 0; i < Config::iSaveStateSlotCount; i++) {
|
||||
SaveSlotView *slot = leftColumnItems->Add(new SaveSlotView(gamePath_, i, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Gravity::G_HCENTER, Margins(0,0,0,0))));
|
||||
SaveSlotView *slot = leftColumnItems->Add(new SaveSlotView(saveStatePrefix_, i, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Gravity::G_HCENTER, Margins(0,0,0,0))));
|
||||
slot->OnStateLoaded.Handle(this, &GamePauseScreen::OnState);
|
||||
slot->OnStateSaved.Handle(this, &GamePauseScreen::OnState);
|
||||
slot->OnScreenshotClicked.Handle(this, &GamePauseScreen::OnScreenshotClicked);
|
||||
slot->OnScreenshotClicked.Add([this](UI::EventParams &e) {
|
||||
SaveSlotView *v = static_cast<SaveSlotView *>(e.v);
|
||||
int slot = v->GetSlot();
|
||||
g_Config.iCurrentStateSlot = v->GetSlot();
|
||||
if (SaveState::HasSaveInSlot(saveStatePrefix_, slot)) {
|
||||
Path fn = v->GetScreenshotFilename();
|
||||
std::string title = v->GetScreenshotTitle();
|
||||
Screen *screen = new ScreenshotViewScreen(fn, saveStatePrefix_, title, v->GetSlot(), gamePath_);
|
||||
screenManager()->push(screen);
|
||||
}
|
||||
});
|
||||
slot->OnSelected.Add([this](UI::EventParams &e) {
|
||||
SaveSlotView *v = static_cast<SaveSlotView *>(e.v);
|
||||
g_Config.iCurrentStateSlot = v->GetSlot();
|
||||
RecreateViews();
|
||||
});
|
||||
}
|
||||
leftColumnItems->Add(new Spacer(0.0));
|
||||
|
||||
LinearLayout *buttonRow = leftColumnItems->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(10, 0, 0, 0))));
|
||||
if (g_Config.bEnableStateUndo && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) {
|
||||
UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load")));
|
||||
loadUndoButton->SetEnabled(SaveState::HasUndoLoad(gamePath_));
|
||||
UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK")));
|
||||
loadUndoButton->SetEnabled(SaveState::HasUndoLoad(saveStatePrefix_));
|
||||
loadUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLoadUndo);
|
||||
|
||||
UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save")));
|
||||
saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(gamePath_));
|
||||
UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save"), ImageID("I_NAVIGATE_BACK")));
|
||||
saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(saveStatePrefix_));
|
||||
saveUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLastSaveUndo);
|
||||
}
|
||||
|
||||
if (g_Config.iRewindSnapshotInterval > 0 && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) {
|
||||
UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind")));
|
||||
UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND")));
|
||||
rewindButton->SetEnabled(SaveState::CanRewind());
|
||||
rewindButton->OnClick.Handle(this, &GamePauseScreen::OnRewind);
|
||||
}
|
||||
@@ -721,18 +745,6 @@ void GamePauseScreen::dialogFinished(const Screen *dialog, DialogResult dr) {
|
||||
}
|
||||
}
|
||||
|
||||
void GamePauseScreen::OnScreenshotClicked(UI::EventParams &e) {
|
||||
SaveSlotView *v = static_cast<SaveSlotView *>(e.v);
|
||||
int slot = v->GetSlot();
|
||||
g_Config.iCurrentStateSlot = v->GetSlot();
|
||||
if (SaveState::HasSaveInSlot(gamePath_, slot)) {
|
||||
Path fn = v->GetScreenshotFilename();
|
||||
std::string title = v->GetScreenshotTitle();
|
||||
Screen *screen = new ScreenshotViewScreen(fn, title, v->GetSlot(), gamePath_);
|
||||
screenManager()->push(screen);
|
||||
}
|
||||
}
|
||||
|
||||
int GetUnsavedProgressSeconds() {
|
||||
const double timeSinceSaveState = SaveState::SecondsSinceLastSavestate();
|
||||
const double timeSinceGameSave = SecondsSinceLastGameSave();
|
||||
@@ -811,13 +823,13 @@ void GamePauseScreen::OnRewind(UI::EventParams &e) {
|
||||
}
|
||||
|
||||
void GamePauseScreen::OnLoadUndo(UI::EventParams &e) {
|
||||
SaveState::UndoLoad(gamePath_, &AfterSaveStateAction);
|
||||
SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction);
|
||||
|
||||
TriggerFinish(DR_CANCEL);
|
||||
}
|
||||
|
||||
void GamePauseScreen::OnLastSaveUndo(UI::EventParams &e) {
|
||||
SaveState::UndoLastSave(gamePath_);
|
||||
SaveState::UndoLastSave(saveStatePrefix_);
|
||||
|
||||
RecreateViews();
|
||||
}
|
||||
|
||||
+2
-2
@@ -53,8 +53,6 @@ private:
|
||||
void OnLoadUndo(UI::EventParams &e);
|
||||
void OnLastSaveUndo(UI::EventParams &e);
|
||||
|
||||
void OnScreenshotClicked(UI::EventParams &e);
|
||||
|
||||
void OnCreateConfig(UI::EventParams &e);
|
||||
void OnDeleteConfig(UI::EventParams &e);
|
||||
|
||||
@@ -77,6 +75,8 @@ private:
|
||||
bool lastDNSConfigLoaded_ = false;
|
||||
|
||||
bool bootPending_ = false;
|
||||
|
||||
std::string saveStatePrefix_;
|
||||
};
|
||||
|
||||
std::string GetConfirmExitMessage();
|
||||
@@ -309,6 +309,7 @@
|
||||
<ClInclude Include="..\..\Core\HLE\Plugins.h" />
|
||||
<ClInclude Include="..\..\Core\RetroAchievements.h" />
|
||||
<ClInclude Include="..\..\Core\SaveState.h" />
|
||||
<ClInclude Include="..\..\Core\SaveStateRewind.h" />
|
||||
<ClInclude Include="..\..\Core\Screenshot.h" />
|
||||
<ClInclude Include="..\..\Core\System.h" />
|
||||
<ClInclude Include="..\..\Core\TiltEventProcessor.h" />
|
||||
@@ -616,6 +617,7 @@
|
||||
<ClCompile Include="..\..\Core\HLE\Plugins.cpp" />
|
||||
<ClCompile Include="..\..\Core\RetroAchievements.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveState.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveStateRewind.cpp" />
|
||||
<ClCompile Include="..\..\Core\Screenshot.cpp" />
|
||||
<ClCompile Include="..\..\Core\System.cpp" />
|
||||
<ClCompile Include="..\..\Core\TiltEventProcessor.cpp" />
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
<ClCompile Include="..\..\ext\xxhash.c" />
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PathUtil.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveStateRewind.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\Core\AVIDump.h" />
|
||||
@@ -683,6 +684,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PathUtil.h" />
|
||||
<ClInclude Include="..\..\Core\SaveStateRewind.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\ext\gason\LICENSE" />
|
||||
|
||||
@@ -335,7 +335,7 @@ namespace MainWindow {
|
||||
RemoveMenu(saveStateSlots, 0, MF_BYPOSITION);
|
||||
}
|
||||
|
||||
auto dt = GetI18NCategory(I18NCat::DESKTOPUI);
|
||||
auto di = GetI18NCategory(I18NCat::DIALOG);
|
||||
// Add new items
|
||||
for (int i = 0; i < Config::iSaveStateSlotCount; ++i) {
|
||||
std::string number = StringFromFormat("%d", i + 1);
|
||||
@@ -343,7 +343,7 @@ namespace MainWindow {
|
||||
// Add an accelerator for the first 10 slots.
|
||||
number = "&" + number;
|
||||
}
|
||||
std::string label = ApplySafeSubstitutions(dt->T("Slot %1"), number);
|
||||
std::string label = ApplySafeSubstitutions(di->T("Slot %1"), number);
|
||||
AppendMenu(
|
||||
saveStateSlots,
|
||||
MF_STRING,
|
||||
@@ -613,7 +613,7 @@ namespace MainWindow {
|
||||
case ID_FILE_QUICKLOADSTATE:
|
||||
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
SetCursor(LoadCursor(0, IDC_WAIT));
|
||||
SaveState::LoadSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -622,7 +622,7 @@ namespace MainWindow {
|
||||
if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
if (!KeyMap::PspButtonHasMappings(VIRTKEY_LOAD_STATE)) {
|
||||
SetCursor(LoadCursor(0, IDC_WAIT));
|
||||
SaveState::LoadSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -631,7 +631,7 @@ namespace MainWindow {
|
||||
{
|
||||
if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) {
|
||||
SetCursor(LoadCursor(0, IDC_WAIT));
|
||||
SaveState::SaveSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -642,7 +642,7 @@ namespace MainWindow {
|
||||
if (!KeyMap::PspButtonHasMappings(VIRTKEY_SAVE_STATE))
|
||||
{
|
||||
SetCursor(LoadCursor(0, IDC_WAIT));
|
||||
SaveState::SaveSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +617,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/Core/Replay.cpp \
|
||||
$(SRC)/Core/RetroAchievements.cpp \
|
||||
$(SRC)/Core/SaveState.cpp \
|
||||
$(SRC)/Core/SaveStateRewind.cpp \
|
||||
$(SRC)/Core/Screenshot.cpp \
|
||||
$(SRC)/Core/System.cpp \
|
||||
$(SRC)/Core/TiltEventProcessor.cpp \
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ContentUri {
|
||||
@@ -65,7 +66,7 @@ public class ContentUri {
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
};
|
||||
|
||||
private static String cursorToString(Cursor c) {
|
||||
private static String cursorToString(Cursor c, String prefixLowercase) {
|
||||
final int flags = c.getInt(2);
|
||||
// Filter out any virtual or partial nonsense.
|
||||
// There's a bunch of potentially-interesting flags here btw,
|
||||
@@ -74,9 +75,16 @@ public class ContentUri {
|
||||
if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) {
|
||||
return null;
|
||||
}
|
||||
final String documentName = c.getString(0);
|
||||
if (prefixLowercase != null && !prefixLowercase.isEmpty()) {
|
||||
// Filter by prefix, case insensitive.
|
||||
if (!documentName.toLowerCase(Locale.ROOT).startsWith(prefixLowercase)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final String mimeType = c.getString(3);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
final String documentName = c.getString(0);
|
||||
final long size = isDirectory ? 0 : c.getLong(1);
|
||||
final long lastModified = c.getLong(4);
|
||||
|
||||
@@ -160,7 +168,7 @@ public class ContentUri {
|
||||
// uestions/42186820/documentfile-is-very-slow
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static String[] listContentUriDir(Activity activity, String uriString) {
|
||||
public static String[] listContentUriDir(Activity activity, String uriString, String prefix) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
@@ -176,12 +184,16 @@ public class ContentUri {
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
if (prefix != null) {
|
||||
prefix = prefix.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) {
|
||||
if (c == null) {
|
||||
return new String[]{"X"};
|
||||
}
|
||||
while (c.moveToNext()) {
|
||||
String str = cursorToString(c);
|
||||
String str = cursorToString(c, prefix);
|
||||
if (str != null) {
|
||||
listing.add(str);
|
||||
}
|
||||
@@ -354,7 +366,7 @@ public class ContentUri {
|
||||
|
||||
try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
return cursorToString(c);
|
||||
return cursorToString(c, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -281,7 +281,6 @@ Savestate Slot = Saves&tate slot
|
||||
Screen Scaling Filter = Scr&een scaling filter
|
||||
Show Debug Statistics = إظهار حالة التصحيح
|
||||
Show FPS Counter = &أظهر عداد الإطارات
|
||||
Slot %1 = فتحة %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &توقف
|
||||
Switch UMD = تغيير UMD
|
||||
@@ -484,6 +483,7 @@ Settings = Settings
|
||||
Shift = شفت
|
||||
Show in folder = أظهر في المجلد
|
||||
Skip = Skip
|
||||
Slot %1 = فتحة %1
|
||||
Snap = Snap
|
||||
Space = مسافة
|
||||
SSID = SSID
|
||||
|
||||
@@ -276,7 +276,6 @@ Show Debug Statistics = Yolaqoyuş Durumlarını &Göstər
|
||||
Show FPS Counter = &FPS Sayğacını Göstər
|
||||
Skip Number of Frames = Kadr buraxma sayı
|
||||
Skip Percent of FPS = FPS Buraxma Yüzdəsi
|
||||
Slot %1 = Yuva %1
|
||||
Smart 2D texture filtering = Ağıllı 2D toxuma süzgüsü
|
||||
Stop = &Dayan
|
||||
Switch UMD = UMD'ni Dəyiş
|
||||
@@ -480,6 +479,7 @@ Settings = Quruluşlar
|
||||
Shift = Shift
|
||||
Show in folder = Qovluqda göstər
|
||||
Skip = Burax
|
||||
Slot %1 = Yuva %1
|
||||
Snap = Oturuşma
|
||||
Space = Ara
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate slot
|
||||
Screen Scaling Filter = Scr&een scaling filter
|
||||
Show Debug Statistics = Show debu&g statistics
|
||||
Show FPS Counter = Show &FPS counter
|
||||
Slot %1 = Слот %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Switch UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Налады
|
||||
Shift = Shift
|
||||
Show in folder = Паказаць у тэчцы
|
||||
Skip = Прапусціць
|
||||
Slot %1 = Слот %1
|
||||
Snap = Snap
|
||||
Space = Прабел
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Брой на запазени състояния
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Покажи Debu&g Statistics
|
||||
Show FPS Counter = Покажи &FPS
|
||||
Slot %1 = Слот %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Смени UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Настройки
|
||||
Shift = Shift
|
||||
Show in folder = Покажи в папка
|
||||
Skip = Пропускане
|
||||
Slot %1 = Слот %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Ranura de desa &d'estats
|
||||
Screen Scaling Filter = Filtre d'escalat de &pantalla
|
||||
Show Debug Statistics = Mostrar estadístiques de &depuació
|
||||
Show FPS Counter = Mostrar comptador de &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Aturar
|
||||
Switch UMD = Canvia UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Canviar
|
||||
Show in folder = Mostrar en carpeta
|
||||
Skip = Saltar
|
||||
Slot %1 = Slot %1
|
||||
Snap = Capturar
|
||||
Space = Espai
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Po&zice uloženého stavu
|
||||
Screen Scaling Filter = Fil&tr změny velikosti obrazovky
|
||||
Show Debug Statistics = Zobrazit &statistiky ladění
|
||||
Show FPS Counter = Zobrazit počíta&dlo
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Zastavit
|
||||
Switch UMD = Přepnout UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Zobrazit ve složce
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Mezera
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = &Lagerplads for spil-status
|
||||
Screen Scaling Filter = Skæ&rmskaleringafilter
|
||||
Show Debug Statistics = &Vis fejlfindingsstatistik
|
||||
Show FPS Counter = Vis &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Skift UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Vis i katalog
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -270,7 +270,6 @@ Savestate Slot = Speicherstand-Slot
|
||||
Screen Scaling Filter = Filter für Bildskalierung
|
||||
Show Debug Statistics = Fehlerbehebungs-Statistiken anzeigen
|
||||
Show FPS Counter = FPS anzeigen
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Intelligente 2D-Texturfilterung
|
||||
Stop = Stopp
|
||||
Switch UMD = UMD wechseln
|
||||
@@ -467,6 +466,7 @@ Settings = Einstellungen
|
||||
Shift = Umschalt
|
||||
Show in folder = Im Ordner anzeigen
|
||||
Skip = Überspringen
|
||||
Slot %1 = Slot %1
|
||||
Snap = Einrasten
|
||||
Space = Leertaste
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Pempakitan Debu&g Statistics
|
||||
Show FPS Counter = Pempakitan &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Kenden
|
||||
Switch UMD = Switch UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Angka'i
|
||||
Show in folder = Show in folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -297,7 +297,6 @@ Savestate Slot = Saves&tate slot
|
||||
Screen Scaling Filter = Scr&een scaling filter
|
||||
Show Debug Statistics = Show debu&g statistics
|
||||
Show FPS Counter = Show &FPS counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Switch UMD
|
||||
@@ -500,6 +499,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Show in folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -274,7 +274,6 @@ Screen Scaling Filter = Filtro de escalado de &pantalla
|
||||
Show Debug Statistics = Mostrar &estadísticas de depuración
|
||||
Show FPS Counter = &Mostrar contador de FPS
|
||||
Skip Buffer Effects = &Saltar efectos de búfer (rápido)
|
||||
Slot %1 = Ranura %1
|
||||
Smart 2D texture filtering = Filtrado inteligente de texturas 2D
|
||||
Stop = Parar
|
||||
Switch UMD = Cambiar UMD
|
||||
@@ -477,6 +476,7 @@ Settings = Ajustes
|
||||
Shift = Alternar
|
||||
Show in folder = Mostrar carpeta
|
||||
Skip = Saltar
|
||||
Slot %1 = Ranura %1
|
||||
Snap = Capturar
|
||||
Space = Espacio
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Ranura de guardar &estados
|
||||
Screen Scaling Filter = Filtro de escalado de &pantalla
|
||||
Show Debug Statistics = Mostrar &estadísticas de depuración
|
||||
Show FPS Counter = &Mostrar contador de FPS
|
||||
Slot %1 = Ranura %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = P&arar
|
||||
Switch UMD = Cambiar &UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Ajustes
|
||||
Shift = Alternar
|
||||
Show in folder = Mostrar en carpeta
|
||||
Skip = Saltar
|
||||
Slot %1 = Ranura %1
|
||||
Snap = Capturar
|
||||
Space = Espacio
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = مکان ذخیره سریع
|
||||
Screen Scaling Filter = فیلتر تغییر سایز صفحه
|
||||
Show Debug Statistics = نمایش وضعیت دیباگ
|
||||
Show FPS Counter = نمایش شمارنده فریم بر ثانیه
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = توقف
|
||||
Switch UMD = تغییر دیسک
|
||||
@@ -476,6 +475,7 @@ Settings = تنظیمات
|
||||
Shift = تغییر
|
||||
Show in folder = نمایش پوشه
|
||||
Skip = رد کردن
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = فاصله
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Tilatallennuksen &lohko
|
||||
Screen Scaling Filter = N&äytön skaalaussuodatin
|
||||
Show Debug Statistics = Näytä &vianetsinnän tiedot
|
||||
Show FPS Counter = Näytä kuvalaskuri (FPS)
|
||||
Slot %1 = Lohko %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Pysäytä
|
||||
Switch UMD = Vaihda UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Asetukset
|
||||
Shift = Shift
|
||||
Show in folder = Näytä kansiossa
|
||||
Skip = Skip
|
||||
Slot %1 = Lohko %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = &Emplacement d'état
|
||||
Screen Scaling Filter = Filtre &de mise à l'échelle de l'écran
|
||||
Show Debug Statistics = &Montrer les statistiques de débogage
|
||||
Show FPS Counter = M&ontrer la vitesse d'émulation
|
||||
Slot %1 = Emplacement %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Arrêter
|
||||
Switch UMD = Changer d'UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Majuscule
|
||||
Show in folder = Montrer dans dossier
|
||||
Skip = Skip
|
||||
Slot %1 = Emplacement %1
|
||||
Snap = Aligner
|
||||
Space = Espace
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Ranura de gardar &estados
|
||||
Screen Scaling Filter = Filtro de escalado de &pantalla
|
||||
Show Debug Statistics = Mostrar &estadísticas de depuración
|
||||
Show FPS Counter = &Mostrar contador de FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = P&arar
|
||||
Switch UMD = Cambiar &UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Alternar
|
||||
Show in folder = Mostrar en carpeta
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Espacio
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Slot Σημείου Αποθήκευσης
|
||||
Screen Scaling Filter = Φίλτρο κλιμάκωσης οθόνης
|
||||
Show Debug Statistics = Εμφάνισ&η Στατιστικών Αποσφαλμάτωσης
|
||||
Show FPS Counter = Εμφάνι&ση μετρητή FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Σταμάτημα
|
||||
Switch UMD = Αλλαγή UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Εμφάνιση σε Φάκελο
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = ΚΕΝΟ
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Show Debu&g Statistics
|
||||
Show FPS Counter = Show &FPS Counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &עצור
|
||||
Switch UMD = Switch UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = העבר
|
||||
Show in folder = Show in folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Show Debu&g Statistics
|
||||
Show FPS Counter = Show &FPS Counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Switch UMD
|
||||
@@ -473,6 +472,7 @@ Settings = Settings
|
||||
Shift = העבר
|
||||
Show in folder = Show in folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate slot
|
||||
Screen Scaling Filter = Scr&een scaling filter
|
||||
Show Debug Statistics = Prikaži debu&g statistike
|
||||
Show FPS Counter = Prikaži &FPS broj
|
||||
Slot %1 = Mjesto %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stopiraj
|
||||
Switch UMD = Promijeni UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Prikaži u mapi
|
||||
Skip = Skip
|
||||
Slot %1 = Mjesto %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Állapotmentés s&orszáma
|
||||
Screen Scaling Filter = Kép&ernyő skálázási szűrő
|
||||
Show Debug Statistics = &Hibakereső statisztikák mutatása
|
||||
Show FPS Counter = &FPS számláló mutatása
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = &Okos 2D textúra szűrés
|
||||
Stop = &Leállítás
|
||||
Switch UMD = UMD &váltása
|
||||
@@ -476,6 +475,7 @@ Settings = Beállítások
|
||||
Shift = Shift
|
||||
Show in folder = Mutatás mappában
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Igazít
|
||||
Space = Szóköz
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Slot saves&tate
|
||||
Screen Scaling Filter = Filter p&enskala layar
|
||||
Show Debug Statistics = Tampilkan statistik debug
|
||||
Show FPS Counter = Tampilkan penghitung &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = Hentikan
|
||||
Switch UMD = Ganti UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Pengaturan
|
||||
Shift = Geser
|
||||
Show in folder = Tampilkan di berkas
|
||||
Skip = Lewati
|
||||
Slot %1 = Slot %1
|
||||
Snap = Garis pinggir
|
||||
Space = Spasi
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Slot del salvataggio di stato
|
||||
Screen Scaling Filter = Filtro Scalatura Schermo
|
||||
Show Debug Statistics = Mostra Statistiche di Debug
|
||||
Show FPS Counter = Mostra FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Filtro texture 2D intelligente
|
||||
Stop = &Interrompi
|
||||
Switch UMD = Cambia UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Impostazioni
|
||||
Shift = Maiuscolo
|
||||
Show in folder = Mostra nella Cartella
|
||||
Skip = Salta
|
||||
Slot %1 = Slot %1
|
||||
Snap = Allinea
|
||||
Space = Spazio
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = セーブステートのスロット (&T)
|
||||
Screen Scaling Filter = 画像スケーリングのフィルタ (&E)
|
||||
Show Debug Statistics = デバッグ情報を表示する (&G)
|
||||
Show FPS Counter = FPSを表示する (&F)
|
||||
Slot %1 = スロット%1
|
||||
Smart 2D texture filtering = スマート2Dテクスチャフィルタリング
|
||||
Stop = 停止 (&S)
|
||||
Switch UMD = UMDを交換する
|
||||
@@ -476,6 +475,7 @@ Settings = 設定
|
||||
Shift = シフト
|
||||
Show in folder = フォルダを表示する
|
||||
Skip = スキップ
|
||||
Slot %1 = スロット%1
|
||||
Snap = グリッドに吸着
|
||||
Space = スペース
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Kalbu Slot
|
||||
Screen Scaling Filter = Layar njongko Filter
|
||||
Show Debug Statistics = Tampilno Statistik Debu&g
|
||||
Show FPS Counter = Tampilno Penghitung &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Mandek
|
||||
Switch UMD = Ngalih UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Tampilno Nang Folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Spasi
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = 상태 저장 슬롯(&T)
|
||||
Screen Scaling Filter = 화면 크기 조정 필터(&E)
|
||||
Show Debug Statistics = 디버그 통계 표시(&G)
|
||||
Show FPS Counter = FPS 카운터 표시(&F)
|
||||
Slot %1 = 슬롯 %1
|
||||
Smart 2D texture filtering = 스마트 2D 텍스처 필터링
|
||||
Stop = 정지(&S)
|
||||
Switch UMD = UMD 교체
|
||||
@@ -476,6 +475,7 @@ Settings = 설정
|
||||
Shift = 전환
|
||||
Show in folder = 폴더에 표기
|
||||
Skip = 건너뜀
|
||||
Slot %1 = 슬롯 %1
|
||||
Snap = 스냅
|
||||
Space = 공간
|
||||
SSID = SSID
|
||||
|
||||
@@ -287,7 +287,6 @@ Savestate Slot = Saves&tate slot
|
||||
Screen Scaling Filter = Scr&een scaling filter
|
||||
Show Debug Statistics = Show debu&g statistics
|
||||
Show FPS Counter = Show &FPS counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Switch UMD
|
||||
@@ -490,6 +489,7 @@ Settings = سێتینگەکان
|
||||
Shift = Shift
|
||||
Show in folder = نیشاندانەوە لە فۆڵدەر
|
||||
Skip = پەڕاندن
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = &ຊ່ອງບັນທຶກເກມ
|
||||
Screen Scaling Filter = &ໂຕຕອງຂະໜາດຂອງໜ້າຈໍ
|
||||
Show Debug Statistics = &ສະແດງຄ່າທາງສະຖິຕິແກ້ໄຂຈຸດບົກພ່ອງ
|
||||
Show FPS Counter = &ສະແດງໂຕນັບຄ່າເຟຣມເຣດ
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &ຢຸດ
|
||||
Switch UMD = ສະຫຼັບແຜ່ນ UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = ປ່ຽນ
|
||||
Show in folder = ສະແດງໃນໂຟນເດີ້
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = ຍະວ່າງ
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Iš&saugojimo statuso vieta
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Rodyti Debu&g statistikas
|
||||
Show FPS Counter = Rodyti &kadrų per sekundę rodmenis
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stabdyti
|
||||
Switch UMD = Perkeisti "UMD"
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Rodyti aplanke
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Tarpas
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Slot Saves&tate
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Papar Stati&stik Pepijat
|
||||
Show FPS Counter = Papar &penghitung FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Henti
|
||||
Switch UMD = Tukar UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shif
|
||||
Show in folder = Papar dalam folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = &Savestatesleuf
|
||||
Screen Scaling Filter = Bee&ldschalingsfilter
|
||||
Show Debug Statistics = &Foutopsporingsstatistieken tonen
|
||||
Show FPS Counter = FP&S-teller weergeven
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stoppen
|
||||
Switch UMD = UMD verwisselen
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Weergeven in map
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Spatie
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Show Debu&g Statistics
|
||||
Show FPS Counter = Vis &bildefrekvensteller
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stopp
|
||||
Switch UMD = Bytt UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Innstillinger
|
||||
Shift = Shift
|
||||
Show in folder = Show in folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -272,7 +272,6 @@ Savestate Slot = Port zapisu stanu
|
||||
Screen Scaling Filter = Filtrowanie skalowania ekranu
|
||||
Show Debug Statistics = Pokaż statystyki debugowania
|
||||
Show FPS Counter = Pokaż licznik FPS
|
||||
Slot %1 = Gniazdo %1
|
||||
Smart 2D texture filtering = Inteligentne filtrowanie tekstur 2D
|
||||
Stop = Zatrzymaj
|
||||
Switch UMD = Podmień UMD
|
||||
@@ -474,6 +473,7 @@ Settings = Ustawienia
|
||||
Shift = Shift
|
||||
Show in folder = Pokaż w folderze
|
||||
Skip = Pomiń
|
||||
Slot %1 = Gniazdo %1
|
||||
Snap = Tryb siatki
|
||||
Space = Spacja
|
||||
SSID = SSID
|
||||
|
||||
@@ -297,7 +297,6 @@ Savestate Slot = Slo&t do save state
|
||||
Screen Scaling Filter = Fi<ro do dimensionamento da tela
|
||||
Show Debug Statistics = Mostrar as estatíst&icas do debug
|
||||
Show FPS Counter = Mostrar o contador de &FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Filtragem inteligente das texturas 2D
|
||||
Stop = &Parar
|
||||
Switch UMD = Trocar UMD
|
||||
@@ -499,6 +498,7 @@ Settings = Configurações
|
||||
Shift = Shift
|
||||
Show in folder = Mostrar na pasta
|
||||
Skip = Ignorar
|
||||
Slot %1 = Slot %1
|
||||
Snap = Grade
|
||||
Space = Espaço
|
||||
SSID = SSID
|
||||
|
||||
@@ -297,7 +297,6 @@ Savestate Slot = Espaç&o do Estado Salvo
|
||||
Screen Scaling Filter = Fi<ro do Dimensionamento da Tela
|
||||
Show Debug Statistics = Mostrar as estatíst&icas de depuração
|
||||
Show FPS Counter = Mostrar o contador de &FPS (Frames Por Segundo)
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Parar
|
||||
Switch UMD = Trocar UMD
|
||||
@@ -500,6 +499,7 @@ Settings = Definições
|
||||
Shift = Shift
|
||||
Show in folder = Mostrar na pasta
|
||||
Skip = Ignorar
|
||||
Slot %1 = Slot %1
|
||||
Snap = Grade
|
||||
Space = Espaço
|
||||
SSID = SSID
|
||||
|
||||
@@ -274,7 +274,6 @@ Savestate Slot = Saves&tate Slot
|
||||
Screen Scaling Filter = Scr&een Scaling Filter
|
||||
Show Debug Statistics = Show Debu&g Statistics
|
||||
Show FPS Counter = Show &FPS Counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = &Stop
|
||||
Switch UMD = Switch UMD
|
||||
@@ -477,6 +476,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Arată în folder
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Слот состояния
|
||||
Screen Scaling Filter = Ф&ильтр масштабирования экрана
|
||||
Show Debug Statistics = Показывать &отладочную информацию
|
||||
Show FPS Counter = По&казывать FPS
|
||||
Slot %1 = Слот %1
|
||||
Smart 2D texture filtering = Умная фильтрация 2D-текстур
|
||||
Stop = Остановить
|
||||
Switch UMD = Сменить UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Настройки
|
||||
Shift = Сдвинуть
|
||||
Show in folder = Показать в папке
|
||||
Skip = Пропустить
|
||||
Slot %1 = Слот %1
|
||||
Snap = Разметка
|
||||
Space = Пробел
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Plats för sparat tillstånd
|
||||
Screen Scaling Filter = Skärmskalningsfilter
|
||||
Show Debug Statistics = Visa felsökningsstatistik
|
||||
Show FPS Counter = Visa räknare för bildfrekvens
|
||||
Slot %1 = Plats %1
|
||||
Smart 2D texture filtering = Smart 2D-texturfiltrering
|
||||
Stop = Stoppa
|
||||
Switch UMD = Växla UMD
|
||||
@@ -477,6 +476,7 @@ Settings = Inställningar
|
||||
Shift = Skift
|
||||
Show in folder = Visa i mapp
|
||||
Skip = Hoppa över
|
||||
Slot %1 = Plats %1
|
||||
Snap = Fäst
|
||||
Space = Mellanslag
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Slot ng save state
|
||||
Screen Scaling Filter = Screen Scaling Filter
|
||||
Show Debug Statistics = Ipakita ang Debug Statistics
|
||||
Show FPS Counter = Ipakita ang FPS Counter
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = I-hinto
|
||||
Switch UMD = Magpalit ng UMD
|
||||
@@ -477,6 +476,7 @@ Settings = Mga Setting
|
||||
Shift = Shift
|
||||
Show in folder = Ipakita sa folder
|
||||
Skip = I-Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = ช่องเซฟสเตทเกม
|
||||
Screen Scaling Filter = ตัวกรองปรับสเกลหน้าจอ
|
||||
Show Debug Statistics = แสดงค่าทางสถิติการแก้ไขจุดบกพร่อง
|
||||
Show FPS Counter = แสดงค่าเฟรมเรทต่อวินาที
|
||||
Slot %1 = เซฟช่องที่ %1
|
||||
Smart 2D texture filtering = ตัวกรองเท็คเจอร์ประเภท 2D แบบชาญฉลาด
|
||||
Stop = หยุด
|
||||
Switch UMD = สลับแผ่น UMD
|
||||
@@ -492,6 +491,7 @@ Settings = การตั้งค่า
|
||||
Shift = เปลี่ยน
|
||||
Show in folder = แสดงในโฟลเดอร์
|
||||
Skip = ข้าม
|
||||
Slot %1 = เซฟช่องที่ %1
|
||||
Snap = ขยับ
|
||||
Space = เว้นวรรค
|
||||
SSID = SSID
|
||||
|
||||
@@ -275,7 +275,6 @@ Savestate Slot = Durum Kaydı Yuvası
|
||||
Screen Scaling Filter = Ekran Ölçek Filtresi
|
||||
Show Debug Statistics = Hata Ayıklama İstatistiklerini Göster
|
||||
Show FPS Counter = FPS Sayacını Göster
|
||||
Slot %1 = Yuva %1
|
||||
Smart 2D texture filtering = Akıllı 2D doku filtresi
|
||||
Stop = Dur
|
||||
Switch UMD = UMD Değiştir
|
||||
@@ -478,6 +477,7 @@ Settings = Ayarlar
|
||||
Shift = Shift
|
||||
Show in folder = Klasörde göster
|
||||
Skip = Geç
|
||||
Slot %1 = Yuva %1
|
||||
Snap = Hizala
|
||||
Space = Boşluk
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Слот збереження
|
||||
Screen Scaling Filter = Ф&ільтр масштабування екрану
|
||||
Show Debug Statistics = Показувати &статистику налагодження
|
||||
Show FPS Counter = По&казувати FPS
|
||||
Slot %1 = Слот %1
|
||||
Smart 2D texture filtering = розумна 2D фільтрація текстур
|
||||
Stop = Зупинити
|
||||
Switch UMD = Змінити UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Параметри
|
||||
Shift = Shift
|
||||
Show in folder = Показати в теці
|
||||
Skip = Пропустити
|
||||
Slot %1 = Слот %1
|
||||
Snap = Знімок
|
||||
Space = Пробіл
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = Ô save
|
||||
Screen Scaling Filter = Bộ lọc họa tiết rộng
|
||||
Show Debug Statistics = Hiện thông số debug
|
||||
Show FPS Counter = Hiện thông số FPS
|
||||
Slot %1 = Slot %1
|
||||
Smart 2D texture filtering = Smart 2D texture filtering
|
||||
Stop = Dừng lại
|
||||
Switch UMD = Chuyển sang UMD
|
||||
@@ -476,6 +475,7 @@ Settings = Settings
|
||||
Shift = Shift
|
||||
Show in folder = Hiện trong thư mục
|
||||
Skip = Skip
|
||||
Slot %1 = Slot %1
|
||||
Snap = Snap
|
||||
Space = Space
|
||||
SSID = SSID
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = 即时存档插槽(&T)
|
||||
Screen Scaling Filter = 屏幕缩放算法(&E)
|
||||
Show Debug Statistics = 显示调试信息(&G)
|
||||
Show FPS Counter = 显示帧率(&F)
|
||||
Slot %1 = 插槽%1
|
||||
Smart 2D texture filtering = 自动保留2D纹理像素风格
|
||||
Stop = 停止(&S)
|
||||
Switch UMD = 切换UMD光盘
|
||||
@@ -476,6 +475,7 @@ Settings = 设置
|
||||
Shift = 翻页
|
||||
Show in folder = 打开文件夹
|
||||
Skip = 是,跳过
|
||||
Slot %1 = 插槽%1
|
||||
Snap = 网格对齐
|
||||
Space = 空格键
|
||||
SSID = 网络名称
|
||||
|
||||
@@ -273,7 +273,6 @@ Savestate Slot = 存檔插槽(&T)
|
||||
Screen Scaling Filter = 螢幕縮放濾鏡(&E)
|
||||
Show Debug Statistics = 顯示偵錯統計資料(&G)
|
||||
Show FPS Counter = 顯示 FPS 計數器(&F)
|
||||
Slot %1 = 插槽 %1
|
||||
Smart 2D texture filtering = 智慧 2D 紋理過濾
|
||||
Stop = 停止(&S)
|
||||
Switch UMD = 切換 UMD
|
||||
@@ -476,6 +475,7 @@ Settings = 設定
|
||||
Shift = 移位
|
||||
Show in folder = 在資料夾中顯示
|
||||
Skip = 跳過
|
||||
Slot %1 = 插槽 %1
|
||||
Snap = 貼齊
|
||||
Space = 空白
|
||||
SSID = SSID
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
viewBox="0 0 158.74998 185.20831"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||
sodipodi:docname="images.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
@@ -24,9 +24,9 @@
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="16.000002"
|
||||
inkscape:cx="338.78121"
|
||||
inkscape:cy="626.46867"
|
||||
inkscape:zoom="2.8284275"
|
||||
inkscape:cx="267.81666"
|
||||
inkscape:cy="394.74231"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2071"
|
||||
inkscape:window-x="-9"
|
||||
@@ -4429,7 +4429,23 @@
|
||||
d="m 73.353766,158.91621 v 2.9068 h 0.968933 v -1.25315 l 1.513086,1.51308 0.685229,-0.68523 -1.513086,-1.51257 h 1.252637 v -0.96893 z m 5.412589,0 v 0.96893 h 1.252636 l -1.551843,1.55185 0.68523,0.68471 1.551843,-1.55184 v 1.25315 h 0.968933 v -2.9068 z m -2.896981,5.11803 -1.546675,1.54668 v -1.25264 h -0.968933 v 2.9068 h 2.906799 v -0.96893 h -1.252637 l 1.547193,-1.54668 z m 3.303674,0.0155 -0.685229,0.68523 1.531172,1.53117 h -1.252636 v 0.96893 h 2.906799 v -2.9068 h -0.968933 v 1.25264 z" /><path
|
||||
id="I_LINK_OUT_QUESTION"
|
||||
style="color:#000000;baseline-shift:baseline;display:inline;overflow:visible;visibility:visible;vector-effect:none;fill:#ffffff;stroke:none;stroke-width:0.70526;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate;stop-color:#000000"
|
||||
d="M 95.057752 156.71186 L 90.723133 156.84622 L 92.339055 158.49211 L 91.223361 159.6631 L 92.174724 160.61447 L 93.289901 159.44296 L 94.924943 161.04751 L 95.057752 156.71186 z M 85.100738 157.98414 L 85.100738 166.83166 L 93.86816 166.83166 L 93.86816 161.78907 L 93.082679 161.78907 L 93.082679 166.04617 L 85.88622 166.04617 L 85.88622 158.76962 L 90.041521 158.76962 L 90.041521 157.98414 L 85.100738 157.98414 z M 89.34699 160.23671 C 89.143515 160.23671 88.946182 160.26034 88.755295 160.30854 C 88.564405 160.35684 88.38765 160.4291 88.226128 160.52558 L 88.226128 161.47333 C 88.383454 161.33492 88.550669 161.23198 88.726873 161.16482 C 88.903078 161.09562 89.072783 161.06095 89.236402 161.06095 C 89.311912 161.06095 89.380206 161.07237 89.441041 161.09557 C 89.503971 161.11857 89.556185 161.14972 89.598137 161.18962 C 89.642187 161.22952 89.675323 161.27825 89.69839 161.33483 C 89.72146 161.38923 89.73353 161.44909 89.73353 161.51415 C 89.73353 161.58965 89.722987 161.65899 89.702007 161.72189 C 89.681027 161.78279 89.649389 161.84249 89.607439 161.90121 C 89.565479 161.95791 89.514283 162.0151 89.453443 162.07381 C 89.394703 162.13041 89.325417 162.19218 89.245704 162.25933 C 89.170184 162.32223 89.103904 162.38651 89.047267 162.45156 C 88.990627 162.51456 88.943433 162.58177 88.905673 162.6531 C 88.867913 162.7223 88.839784 162.79663 88.820924 162.87634 C 88.802044 162.95604 88.792502 163.04391 88.792502 163.14041 C 88.792502 163.20131 88.797942 163.26653 88.808522 163.33575 C 88.821102 163.40285 88.837668 163.46364 88.858648 163.51816 L 89.685987 163.51816 C 89.662917 163.48246 89.64435 163.43879 89.62966 163.38639 C 89.61708 163.33399 89.611056 163.28274 89.611056 163.23239 C 89.611056 163.15899 89.620152 163.09368 89.638962 163.03706 C 89.657842 162.97836 89.68495 162.92359 89.72061 162.87324 C 89.75837 162.82084 89.804573 162.76807 89.859103 162.71563 C 89.915743 162.66313 89.981021 162.60693 90.05444 162.54613 C 90.16352 162.45593 90.25992 162.36855 90.343828 162.28465 C 90.427728 162.19865 90.49803 162.10978 90.554668 162.01748 C 90.611308 161.92518 90.654489 161.82777 90.683859 161.72499 C 90.713229 161.6201 90.727784 161.50361 90.727784 161.37566 C 90.727784 161.17428 90.692656 161.00205 90.621331 160.85941 C 90.552101 160.71467 90.453668 160.59618 90.327808 160.50388 C 90.204044 160.41168 90.058441 160.34431 89.890626 160.30234 C 89.722812 160.25834 89.542074 160.23671 89.34699 160.23671 z M 89.318568 163.85819 C 89.150754 163.85819 89.011166 163.90732 88.899989 164.00599 C 88.788812 164.10459 88.733074 164.22761 88.733074 164.37444 C 88.733074 164.51709 88.788816 164.64061 88.899989 164.74548 C 89.011166 164.84622 89.150754 164.89637 89.318568 164.89637 C 89.486383 164.89637 89.62445 164.84715 89.73353 164.74858 C 89.840512 164.64998 89.893726 164.52548 89.893726 164.37444 C 89.893726 164.22551 89.838996 164.10248 89.729912 164.00599 C 89.620833 163.90739 89.484284 163.85819 89.318568 163.85819 z " /></g><path
|
||||
d="M 95.057752 156.71186 L 90.723133 156.84622 L 92.339055 158.49211 L 91.223361 159.6631 L 92.174724 160.61447 L 93.289901 159.44296 L 94.924943 161.04751 L 95.057752 156.71186 z M 85.100738 157.98414 L 85.100738 166.83166 L 93.86816 166.83166 L 93.86816 161.78907 L 93.082679 161.78907 L 93.082679 166.04617 L 85.88622 166.04617 L 85.88622 158.76962 L 90.041521 158.76962 L 90.041521 157.98414 L 85.100738 157.98414 z M 89.34699 160.23671 C 89.143515 160.23671 88.946182 160.26034 88.755295 160.30854 C 88.564405 160.35684 88.38765 160.4291 88.226128 160.52558 L 88.226128 161.47333 C 88.383454 161.33492 88.550669 161.23198 88.726873 161.16482 C 88.903078 161.09562 89.072783 161.06095 89.236402 161.06095 C 89.311912 161.06095 89.380206 161.07237 89.441041 161.09557 C 89.503971 161.11857 89.556185 161.14972 89.598137 161.18962 C 89.642187 161.22952 89.675323 161.27825 89.69839 161.33483 C 89.72146 161.38923 89.73353 161.44909 89.73353 161.51415 C 89.73353 161.58965 89.722987 161.65899 89.702007 161.72189 C 89.681027 161.78279 89.649389 161.84249 89.607439 161.90121 C 89.565479 161.95791 89.514283 162.0151 89.453443 162.07381 C 89.394703 162.13041 89.325417 162.19218 89.245704 162.25933 C 89.170184 162.32223 89.103904 162.38651 89.047267 162.45156 C 88.990627 162.51456 88.943433 162.58177 88.905673 162.6531 C 88.867913 162.7223 88.839784 162.79663 88.820924 162.87634 C 88.802044 162.95604 88.792502 163.04391 88.792502 163.14041 C 88.792502 163.20131 88.797942 163.26653 88.808522 163.33575 C 88.821102 163.40285 88.837668 163.46364 88.858648 163.51816 L 89.685987 163.51816 C 89.662917 163.48246 89.64435 163.43879 89.62966 163.38639 C 89.61708 163.33399 89.611056 163.28274 89.611056 163.23239 C 89.611056 163.15899 89.620152 163.09368 89.638962 163.03706 C 89.657842 162.97836 89.68495 162.92359 89.72061 162.87324 C 89.75837 162.82084 89.804573 162.76807 89.859103 162.71563 C 89.915743 162.66313 89.981021 162.60693 90.05444 162.54613 C 90.16352 162.45593 90.25992 162.36855 90.343828 162.28465 C 90.427728 162.19865 90.49803 162.10978 90.554668 162.01748 C 90.611308 161.92518 90.654489 161.82777 90.683859 161.72499 C 90.713229 161.6201 90.727784 161.50361 90.727784 161.37566 C 90.727784 161.17428 90.692656 161.00205 90.621331 160.85941 C 90.552101 160.71467 90.453668 160.59618 90.327808 160.50388 C 90.204044 160.41168 90.058441 160.34431 89.890626 160.30234 C 89.722812 160.25834 89.542074 160.23671 89.34699 160.23671 z M 89.318568 163.85819 C 89.150754 163.85819 89.011166 163.90732 88.899989 164.00599 C 88.788812 164.10459 88.733074 164.22761 88.733074 164.37444 C 88.733074 164.51709 88.788816 164.64061 88.899989 164.74548 C 89.011166 164.84622 89.150754 164.89637 89.318568 164.89637 C 89.486383 164.89637 89.62445 164.84715 89.73353 164.74858 C 89.840512 164.64998 89.893726 164.52548 89.893726 164.37444 C 89.893726 164.22551 89.838996 164.10248 89.729912 164.00599 C 89.620833 163.90739 89.484284 163.85819 89.318568 163.85819 z " /><g
|
||||
id="I_REWIND"
|
||||
inkscape:export-xdpi="133.41901"
|
||||
inkscape:export-ydpi="133.41901"
|
||||
transform="matrix(-0.32773772,0,0,0.32773772,161.91901,16.728908)"><path
|
||||
sodipodi:nodetypes="cccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path114"
|
||||
d="m 303.9714,291.491 -10.02611,12.69616 v -25.39233 z"
|
||||
inkscape:transform-center-x="-3.6639545"
|
||||
style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none" /><path
|
||||
style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
|
||||
inkscape:transform-center-x="-3.6639545"
|
||||
d="m 316.82854,291.491 -10.02611,12.69616 v -25.39233 z"
|
||||
id="path117"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccc" /></g></g><path
|
||||
id="I_GEAR"
|
||||
style="fill:#ececec;fill-opacity:1;stroke-width:0.019487"
|
||||
class="st0"
|
||||
|
||||
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 236 KiB |
@@ -879,6 +879,7 @@ SOURCES_CXX += \
|
||||
$(COREDIR)/Reporting.cpp \
|
||||
$(COREDIR)/RetroAchievements.cpp \
|
||||
$(COREDIR)/SaveState.cpp \
|
||||
$(COREDIR)/SaveStateRewind.cpp \
|
||||
$(COREDIR)/Screenshot.cpp \
|
||||
$(COREDIR)/System.cpp \
|
||||
$(COREDIR)/Util/AtracTrack.cpp \
|
||||
|
||||
Reference in new issue
Block a user