diff --git a/Common/File/DirListing.h b/Common/File/DirListing.h
index fff053923a..974f5e13a1 100644
--- a/Common/File/DirListing.h
+++ b/Common/File/DirListing.h
@@ -24,6 +24,10 @@ struct FileInfo {
uint64_t ctime;
uint32_t access; // st_mode & 0x1ff
+ // Currently only supported for Android storage files.
+ // Other places use different methods to get this.
+ uint64_t lastModified = 0;
+
bool operator <(const FileInfo &other) const;
};
diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj
index ececc51b5f..8e6c9ff709 100644
--- a/Core/Core.vcxproj
+++ b/Core/Core.vcxproj
@@ -529,6 +529,7 @@
+
@@ -1085,6 +1086,7 @@
+
diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters
index 43a36fa022..e54ac5b2ea 100644
--- a/Core/Core.vcxproj.filters
+++ b/Core/Core.vcxproj.filters
@@ -962,6 +962,9 @@
Debugger\WebSocket
+
+ FileSystems
+
Ext\libzip
@@ -1874,6 +1877,9 @@
Debugger\WebSocket
+
+ FileSystems
+
Ext\libzip
@@ -1918,4 +1924,4 @@
Ext\libzip
-
\ No newline at end of file
+
diff --git a/Core/FileSystems/AndroidStorageFileSystem.cpp b/Core/FileSystems/AndroidStorageFileSystem.cpp
new file mode 100644
index 0000000000..60e2663696
--- /dev/null
+++ b/Core/FileSystems/AndroidStorageFileSystem.cpp
@@ -0,0 +1,539 @@
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#include "ppsspp_config.h"
+
+#if PPSSPP_PLATFORM(ANDROID)
+#include "android/jni/app-android.h"
+#endif
+
+#include
+#include
+#include
+
+#include "Common/Data/Text/I18n.h"
+#include "Common/Data/Encoding/Utf8.h"
+#include "Common/Serialize/Serializer.h"
+#include "Common/Serialize/SerializeFuncs.h"
+#include "Common/StringUtils.h"
+#include "Common/File/DirListing.h"
+#include "Core/FileSystems/AndroidStorageFileSystem.h"
+#include "Core/HLE/sceKernel.h"
+#include "Core/HW/MemoryStick.h"
+#include "Core/CoreTiming.h"
+#include "Core/System.h"
+#include "Core/Host.h"
+#include "Core/Replay.h"
+#include "Core/Reporting.h"
+
+AndroidStorageFileSystem::AndroidStorageFileSystem(IHandleAllocator *_hAlloc, std::string _basePath, FileSystemFlags _flags) : basePath(_basePath), flags(_flags) {
+ // File::CreateFullPath(basePath);
+ hAlloc = _hAlloc;
+}
+
+AndroidStorageFileSystem::~AndroidStorageFileSystem() {
+ CloseAll();
+}
+
+bool AndroidDirectoryFileHandle::Open(const std::string &basePath, std::string &fileName, FileAccess access, u32 &error) {
+ error = 0;
+
+ // On the PSP, truncating doesn't lose data. If you seek later, you'll recover it.
+ // This is abnormal, so we deviate from the PSP's behavior and truncate on write/close.
+ // This means it's incorrectly not truncated before the write.
+ if (access & FILEACCESS_TRUNCATE) {
+ needsTrunc_ = 0;
+ }
+
+ //TODO: tests, should append seek to end of file? seeking in a file opened for append?
+
+ int flags = 0;
+ if (access & FILEACCESS_APPEND) {
+ flags |= O_APPEND;
+ }
+ if ((access & FILEACCESS_READ) && (access & FILEACCESS_WRITE)) {
+ flags |= O_RDWR;
+ } else if (access & FILEACCESS_READ) {
+ flags |= O_RDONLY;
+ } else if (access & FILEACCESS_WRITE) {
+ flags |= O_WRONLY;
+ }
+ if (access & FILEACCESS_CREATE) {
+ flags |= O_CREAT;
+ }
+ if (access & FILEACCESS_EXCL) {
+ flags |= O_EXCL;
+ }
+
+ hFile = open(fullName.c_str(), flags, 0666);
+ bool success = hFile != -1;
+
+#if HOST_IS_CASE_SENSITIVE
+ if (!success && !(access & FILEACCESS_CREATE)) {
+ if (!FixPathCase(basePath, fileName, FPC_PATH_MUST_EXIST)) {
+ error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
+ return false;
+ }
+ fullName = GetLocalPath(basePath, fileName);
+ const char *fullNameC = fullName.c_str();
+
+ DEBUG_LOG(FILESYS, "Case may have been incorrect, second try opening %s (%s)", fullNameC, fileName.c_str());
+
+ // And try again with the correct case this time
+#ifdef _WIN32
+ hFile = CreateFile(fullNameC, desired, sharemode, 0, openmode, 0, 0);
+ success = hFile != INVALID_HANDLE_VALUE;
+#else
+ hFile = open(fullNameC, flags, 0666);
+ success = hFile != -1;
+#endif
+ }
+#endif
+
+#ifndef _WIN32
+ if (success) {
+ // Reject directories, even if we succeed in opening them.
+ // TODO: Might want to do this stat first...
+ struct stat st;
+ if (fstat(hFile, &st) == 0 && S_ISDIR(st.st_mode)) {
+ close(hFile);
+ errno = EISDIR;
+ success = false;
+ }
+ } else if (errno == ENOSPC) {
+ // This is returned when the disk is full.
+ auto err = GetI18NCategory("Error");
+ host->NotifyUserMessage(err->T("Disk full while writing data"));
+ error = SCE_KERNEL_ERROR_ERRNO_NO_PERM;
+ } else {
+ error = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
+ }
+#endif
+
+ // Try to detect reads/writes to PSP/GAME to avoid them in replays.
+ if (fullName.find("/PSP/GAME/") != fullName.npos || fullName.find("\\PSP\\GAME\\") != fullName.npos) {
+ inGameDir_ = true;
+ }
+
+ return success;
+}
+
+size_t AndroidDirectoryFileHandle::Read(u8* pointer, s64 size)
+{
+ size_t bytesRead = 0;
+ if (needsTrunc_ != -1) {
+ // If the file was marked to be truncated, pretend there's nothing.
+ // On a PSP. it actually is truncated, but the data wasn't erased.
+ off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);
+ if (needsTrunc_ <= off) {
+ return replay_ ? ReplayApplyDiskRead(pointer, 0, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : 0;
+ }
+ if (needsTrunc_ < off + size) {
+ size = needsTrunc_ - off;
+ }
+ }
+ bytesRead = read(hFile, pointer, size);
+ return replay_ ? ReplayApplyDiskRead(pointer, (uint32_t)bytesRead, (uint32_t)size, inGameDir_, CoreTiming::GetGlobalTimeUs()) : bytesRead;
+}
+
+size_t AndroidDirectoryFileHandle::Write(const u8* pointer, s64 size)
+{
+ size_t bytesWritten = 0;
+ bool diskFull = false;
+
+ bytesWritten = write(hFile, pointer, size);
+ if (bytesWritten == (size_t)-1) {
+ diskFull = errno == ENOSPC;
+ }
+ if (needsTrunc_ != -1) {
+ off_t off = (off_t)Seek(0, FILEMOVE_CURRENT);
+ if (needsTrunc_ < off) {
+ needsTrunc_ = off;
+ }
+ }
+
+ if (replay_) {
+ bytesWritten = ReplayApplyDiskWrite(pointer, (uint64_t)bytesWritten, (uint64_t)size, &diskFull, inGameDir_, CoreTiming::GetGlobalTimeUs());
+ }
+
+ if (diskFull) {
+ ERROR_LOG(FILESYS, "Disk full");
+ auto err = GetI18NCategory("Error");
+ host->NotifyUserMessage(err->T("Disk full while writing data"));
+ // We only return an error when the disk is actually full.
+ // When writing this would cause the disk to be full, so it wasn't written, we return 0.
+ if (MemoryStick_FreeSpace() == 0) {
+ // Sign extend on 64-bit.
+ return (size_t)(s64)(s32)SCE_KERNEL_ERROR_ERRNO_DEVICE_NO_FREE_SPACE;
+ }
+ }
+
+ return bytesWritten;
+}
+
+size_t AndroidDirectoryFileHandle::Seek(s32 position, FileMove type)
+{
+ if (needsTrunc_ != -1) {
+ // If the file is "currently truncated" move to the end based on that position.
+ // The actual, underlying file hasn't been truncated (yet.)
+ if (type == FILEMOVE_END) {
+ type = FILEMOVE_BEGIN;
+ position = needsTrunc_ + position;
+ }
+ }
+
+ size_t result;
+
+ int moveMethod = 0;
+ switch (type) {
+ case FILEMOVE_BEGIN: moveMethod = SEEK_SET; break;
+ case FILEMOVE_CURRENT: moveMethod = SEEK_CUR; break;
+ case FILEMOVE_END: moveMethod = SEEK_END; break;
+ }
+ result = lseek(hFile, position, moveMethod);
+
+ return replay_ ? (size_t)ReplayApplyDisk64(ReplayAction::FILE_SEEK, result, CoreTiming::GetGlobalTimeUs()) : result;
+}
+
+void AndroidDirectoryFileHandle::Close()
+{
+ if (needsTrunc_ != -1) {
+ // Note: it's not great that Switch cannot truncate appropriately...
+ if (ftruncate(hFile, (off_t)needsTrunc_) != 0) {
+ ERROR_LOG_REPORT(FILESYS, "Failed to truncate file.");
+ }
+ }
+ if (hFile != -1)
+ close(hFile);
+}
+
+void AndroidStorageFileSystem::CloseAll() {
+ for (auto iter = entries.begin(); iter != entries.end(); ++iter) {
+ INFO_LOG(FILESYS, "DirectoryFileSystem::CloseAll(): Force closing %d (%s)", (int)iter->first, iter->second.guestFilename.c_str());
+ iter->second.hFile.Close();
+ }
+ entries.clear();
+}
+
+std::string AndroidStorageFileSystem::GetLocalPath(std::string localpath) {
+ if (localpath.empty()) {
+ return baseContentUri.ToString();
+ }
+
+ if (localpath[0] == '/')
+ localpath.erase(0, 1);
+
+ return baseContentUri.WithFilePath(localpath).ToString();
+}
+
+bool AndroidStorageFileSystem::MkDir(const std::string &dirname) {
+ ERROR_LOG(FILESYS, "MkDir operation not yet supported.");
+ // TODO: Figure out a way to create directories in Storage...
+ // TODO: Use Android_CreateDirectory. Not sure how deep it can go...
+ bool result = false;
+ // bool result = File::CreateFullPath(GetLocalPath(dirname));
+ return ReplayApplyDisk(ReplayAction::MKDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;
+}
+
+bool AndroidStorageFileSystem::RmDir(const std::string &dirname) {
+
+ ERROR_LOG(FILESYS, "RmDir operation not yet supported.");
+
+ return false; // ReplayApplyDisk(ReplayAction::RMDIR, result, CoreTiming::GetGlobalTimeUs()) != 0;
+}
+
+int AndroidStorageFileSystem::RenameFile(const std::string &from, const std::string &to) {
+ std::string fullTo = to;
+
+ // Rename ignores the path (even if specified) on to.
+ size_t chop_at = to.find_last_of('/');
+ if (chop_at != to.npos)
+ fullTo = to.substr(chop_at + 1);
+
+ // Now put it in the same directory as from.
+ size_t dirname_end = from.find_last_of('/');
+ if (dirname_end != from.npos)
+ fullTo = from.substr(0, dirname_end + 1) + fullTo;
+
+ // At this point, we should check if the paths match and give an already exists error.
+ if (from == fullTo)
+ return ReplayApplyDisk(ReplayAction::FILE_RENAME, SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS, CoreTiming::GetGlobalTimeUs());
+
+ std::string fullFrom = GetLocalPath(from);
+
+ fullTo = GetLocalPath(fullTo);
+ const char * fullToC = fullTo.c_str();
+
+ bool retValue = (0 == rename(fullFrom.c_str(), fullToC));
+
+ // TODO: Better error codes.
+ int result = retValue ? 0 : (int)SCE_KERNEL_ERROR_ERRNO_FILE_ALREADY_EXISTS;
+ return ReplayApplyDisk(ReplayAction::FILE_RENAME, result, CoreTiming::GetGlobalTimeUs());
+}
+
+bool AndroidStorageFileSystem::RemoveFile(const std::string &filename) {
+ std::string fullName = GetLocalPath(filename);
+ bool retValue = (0 == unlink(fullName.c_str()));
+ return ReplayApplyDisk(ReplayAction::FILE_REMOVE, retValue, CoreTiming::GetGlobalTimeUs()) != 0;
+}
+
+int AndroidStorageFileSystem::OpenFile(std::string filename, FileAccess access, const char *devicename) {
+ OpenFileEntry entry;
+ u32 err = 0;
+ bool success = entry.hFile.Open(basePath, filename, access, err);
+ if (err == 0 && !success) {
+ err = SCE_KERNEL_ERROR_ERRNO_FILE_NOT_FOUND;
+ }
+
+ err = ReplayApplyDisk(ReplayAction::FILE_OPEN, err, CoreTiming::GetGlobalTimeUs());
+ if (err != 0) {
+ ERROR_LOG(FILESYS, "DirectoryFileSystem::OpenFile: FAILED, %i - access = %i", errno, (int)access);
+ return err;
+ } else {
+ if (access & FILEACCESS_APPEND)
+ entry.hFile.Seek(0, FILEMOVE_END);
+
+ u32 newHandle = hAlloc->GetNewHandle();
+
+ entry.guestFilename = filename;
+ entry.access = access;
+
+ entries[newHandle] = entry;
+
+ return newHandle;
+ }
+}
+
+void AndroidStorageFileSystem::CloseFile(u32 handle) {
+ EntryMap::iterator iter = entries.find(handle);
+ if (iter != entries.end()) {
+ hAlloc->FreeHandle(handle);
+ iter->second.hFile.Close();
+ entries.erase(iter);
+ } else {
+ // This shouldn't happen...
+ ERROR_LOG(FILESYS, "Cannot close file that hasn't been opened: %08x", handle);
+ }
+}
+
+bool AndroidStorageFileSystem::OwnsHandle(u32 handle) {
+ EntryMap::iterator iter = entries.find(handle);
+ return (iter != entries.end());
+}
+
+int AndroidStorageFileSystem::Ioctl(u32 handle, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec) {
+ return SCE_KERNEL_ERROR_ERRNO_FUNCTION_NOT_SUPPORTED;
+}
+
+PSPDevType AndroidStorageFileSystem::DevType(u32 handle) {
+ return PSPDevType::FILE;
+}
+
+size_t AndroidStorageFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size) {
+ int ignored;
+ return ReadFile(handle, pointer, size, ignored);
+}
+
+size_t AndroidStorageFileSystem::ReadFile(u32 handle, u8 *pointer, s64 size, int &usec) {
+ EntryMap::iterator iter = entries.find(handle);
+ if (iter != entries.end()) {
+ if (size < 0) {
+ ERROR_LOG_REPORT(FILESYS, "Invalid read for %lld bytes from disk %s", size, iter->second.guestFilename.c_str());
+ return 0;
+ }
+
+ size_t bytesRead = iter->second.hFile.Read(pointer, size);
+ return bytesRead;
+ } else {
+ //This shouldn't happen...
+ ERROR_LOG(FILESYS, "Cannot read file that hasn't been opened: %08x", handle);
+ return 0;
+ }
+}
+
+size_t AndroidStorageFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size) {
+ int ignored;
+ return WriteFile(handle, pointer, size, ignored);
+}
+
+size_t AndroidStorageFileSystem::WriteFile(u32 handle, const u8 *pointer, s64 size, int &usec) {
+ EntryMap::iterator iter = entries.find(handle);
+ if (iter != entries.end())
+ {
+ size_t bytesWritten = iter->second.hFile.Write(pointer, size);
+ return bytesWritten;
+ } else {
+ //This shouldn't happen...
+ ERROR_LOG(FILESYS, "Cannot write to file that hasn't been opened: %08x", handle);
+ return 0;
+ }
+}
+
+size_t AndroidStorageFileSystem::SeekFile(u32 handle, s32 position, FileMove type) {
+ EntryMap::iterator iter = entries.find(handle);
+ if (iter != entries.end()) {
+ return iter->second.hFile.Seek(position, type);
+ } else {
+ //This shouldn't happen...
+ ERROR_LOG(FILESYS, "Cannot seek in file that hasn't been opened: %08x", handle);
+ return 0;
+ }
+}
+
+PSPFileInfo AndroidStorageFileSystem::GetFileInfo(std::string filename) {
+ PSPFileInfo x;
+ x.name = filename;
+
+ std::string uri = GetLocalPath(filename);
+
+ FileInfo info;
+ if (!Android_GetFileInfo(uri, &info)) {
+ return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
+ }
+
+ x.type = info.isDirectory ? FILETYPE_DIRECTORY : FILETYPE_NORMAL;
+ x.exists = true;
+
+ if (x.type != FILETYPE_DIRECTORY) {
+ x.size = info.size;
+ x.access = info.isWritable ? 0777 : 0666;
+
+ // The only time value we get from the storage API.
+ int64_t lastModified = info.lastModified / 1000;
+
+ time_t atime = lastModified;
+ time_t ctime = lastModified;
+ time_t mtime = lastModified;
+
+ localtime_r((time_t*)&atime, &x.atime);
+ localtime_r((time_t*)&ctime, &x.ctime);
+ localtime_r((time_t*)&mtime, &x.mtime);
+ }
+
+ return ReplayApplyDiskFileInfo(x, CoreTiming::GetGlobalTimeUs());
+}
+
+bool AndroidStorageFileSystem::GetHostPath(const std::string &inpath, std::string &outpath) {
+ outpath = GetLocalPath(inpath);
+ return true;
+}
+
+// See comment in DirectoryFileSystem.
+extern std::string SimulateVFATBug(std::string filename);
+
+std::vector AndroidStorageFileSystem::GetDirListing(std::string path) {
+ std::vector myVector;
+ bool listingRoot = path == "/" || path == "\\";
+
+ std::string uri = GetLocalPath(path);
+
+ std::vector fileInfo = Android_ListContentUri(uri);
+
+ bool hideISOFiles = PSP_CoreParameter().compat.flags().HideISOFiles;
+ for (auto &info : fileInfo) {
+ PSPFileInfo entry;
+ if (info.isDirectory)
+ entry.type = FILETYPE_DIRECTORY;
+ else
+ entry.type = FILETYPE_NORMAL;
+ entry.access = info.isWritable ? 0777 : 0666;
+ entry.name = info.name;
+ if (Flags() & FileSystemFlags::SIMULATE_FAT32)
+ entry.name = SimulateVFATBug(entry.name);
+ entry.size = info.size;
+
+ bool hideFile = false;
+ if (hideISOFiles && (endsWithNoCase(entry.name, ".cso") || endsWithNoCase(entry.name, ".iso"))) {
+ // Workaround for DJ Max Portable, see compat.ini.
+ hideFile = true;
+ }
+ int64_t lastModified = info.lastModified / 1000;
+
+ time_t atime = lastModified;
+ time_t ctime = lastModified;
+ time_t mtime = lastModified;
+
+ localtime_r((time_t*)&s.st_atime, &entry.atime);
+ localtime_r((time_t*)&s.st_ctime, &entry.ctime);
+ localtime_r((time_t*)&s.st_mtime, &entry.mtime);
+ if (!hideFile && (!listingRoot || (strcmp(info.name.c_str(), "..") && strcmp(info.name.c_str(), "."))))
+ myVector.push_back(entry);
+ }
+
+ return ReplayApplyDiskListing(myVector, CoreTiming::GetGlobalTimeUs());
+}
+
+u64 AndroidStorageFileSystem::FreeSpace(const std::string &path) {
+ // Can't get this, I think.
+
+ return ReplayApplyDisk64(ReplayAction::FREESPACE, std::numeric_limits::max(), CoreTiming::GetGlobalTimeUs());
+}
+
+// WARNING! This must be kept compatible with DirectoryFileSystem for save state portability.
+void AndroidStorageFileSystem::DoState(PointerWrap &p) {
+ auto s = p.Section("DirectoryFileSystem", 0, 2);
+ if (!s)
+ return;
+
+ // Savestate layout:
+ // u32: number of entries
+ // per-entry:
+ // u32: handle number
+ // std::string filename (in guest's terms, untranslated)
+ // enum FileAccess file access mode
+ // u32 seek position
+ // s64 current truncate position (v2+ only)
+
+ u32 num = (u32)entries.size();
+ Do(p, num);
+
+ if (p.mode == p.MODE_READ) {
+ CloseAll();
+ u32 key;
+ OpenFileEntry entry;
+ for (u32 i = 0; i < num; i++) {
+ Do(p, key);
+ Do(p, entry.guestFilename);
+ Do(p, entry.access);
+ u32 err;
+ if (!entry.hFile.Open(basePath, entry.guestFilename, entry.access, err)) {
+ ERROR_LOG(FILESYS, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str());
+ continue;
+ }
+ u32 position;
+ Do(p, position);
+ if (position != entry.hFile.Seek(position, FILEMOVE_BEGIN)) {
+ ERROR_LOG(FILESYS, "Failed to restore seek position while loading state: %s", entry.guestFilename.c_str());
+ continue;
+ }
+ if (s >= 2) {
+ Do(p, entry.hFile.needsTrunc_);
+ }
+ entries[key] = entry;
+ }
+ } else {
+ for (auto iter = entries.begin(); iter != entries.end(); ++iter) {
+ u32 key = iter->first;
+ Do(p, key);
+ Do(p, iter->second.guestFilename);
+ Do(p, iter->second.access);
+ u32 position = (u32)iter->second.hFile.Seek(0, FILEMOVE_CURRENT);
+ Do(p, position);
+ Do(p, iter->second.hFile.needsTrunc_);
+ }
+ }
+}
diff --git a/Core/FileSystems/AndroidStorageFileSystem.h b/Core/FileSystems/AndroidStorageFileSystem.h
new file mode 100644
index 0000000000..25fceff1dd
--- /dev/null
+++ b/Core/FileSystems/AndroidStorageFileSystem.h
@@ -0,0 +1,105 @@
+#pragma once
+
+// Like DirectoryFileSystem, but uses the Android Storage Access Framework
+// to access a folder tree as if it was a PSP file system.
+// Unfortunately we cannot implement all the semantics like this, let's see
+// how good we can get it though.
+
+// Copyright (c) 2012- PPSSPP Project.
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 2.0 or later versions.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License 2.0 for more details.
+
+// A copy of the GPL 2.0 should have been included with the program.
+// If not, see http://www.gnu.org/licenses/
+
+// Official git repository and contact information can be found at
+// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+
+#pragma once
+
+#include "ppsspp_config.h"
+
+#include