mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Merge pull request #17725 from hrydgard/custom-achievement-sound-effects
Custom achievement sound effects
This commit is contained in:
22 files changed
+302
-79
No files matched your search
@@ -4,6 +4,7 @@
|
||||
#include "Common/File/VFS/VFS.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/File/AndroidStorage.h"
|
||||
#include "Common/StringUtils.h"
|
||||
|
||||
VFS g_VFS;
|
||||
|
||||
@@ -27,7 +28,7 @@ void VFS::Clear() {
|
||||
static bool IsLocalAbsolutePath(const char *path) {
|
||||
bool isUnixLocal = path[0] == '/';
|
||||
#ifdef _WIN32
|
||||
bool isWindowsLocal = isalpha(path[0]) && path[1] == ':';
|
||||
bool isWindowsLocal = (isalpha(path[0]) && path[1] == ':') || startsWith(path, "\\\\") || startsWith(path, "//");
|
||||
#else
|
||||
bool isWindowsLocal = false;
|
||||
#endif
|
||||
|
||||
@@ -90,6 +90,7 @@ enum class BrowseFileType {
|
||||
IMAGE,
|
||||
INI,
|
||||
DB,
|
||||
SOUND_EFFECT,
|
||||
ANY,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,16 +51,6 @@ public:
|
||||
|
||||
IconCacheStats GetStats();
|
||||
|
||||
// for testing
|
||||
std::string GetFirstIconName() const {
|
||||
if (!cache_.empty()) {
|
||||
return cache_.begin()->first;
|
||||
} else if (!pending_.empty()) {
|
||||
return *pending_.begin();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private:
|
||||
void Decimate(int64_t maxSize);
|
||||
|
||||
|
||||
@@ -597,7 +597,7 @@ void AbstractChoiceWithValueDisplay::Draw(UIContext &dc) {
|
||||
|
||||
std::string valueText = ValueText();
|
||||
|
||||
if (password_) {
|
||||
if (passwordDisplay_) {
|
||||
// Replace all characters with stars.
|
||||
memset(&valueText[0], '*', valueText.size());
|
||||
}
|
||||
@@ -654,4 +654,28 @@ std::string ChoiceWithValueDisplay::ValueText() const {
|
||||
return valueText.str();
|
||||
}
|
||||
|
||||
FileChooserChoice::FileChooserChoice(std::string *value, const std::string &text, BrowseFileType fileType, LayoutParams *layoutParams)
|
||||
: AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), fileType_(fileType) {
|
||||
OnClick.Add([=](UI::EventParams &) {
|
||||
System_BrowseForFile(text_, fileType, [=](const std::string &returnValue, int) {
|
||||
if (*value_ != returnValue) {
|
||||
*value = returnValue;
|
||||
UI::EventParams e{};
|
||||
e.s = *value;
|
||||
OnChange.Trigger(e);
|
||||
}
|
||||
});
|
||||
return UI::EVENT_DONE;
|
||||
});
|
||||
}
|
||||
|
||||
std::string FileChooserChoice::ValueText() const {
|
||||
if (value_->empty()) {
|
||||
auto di = GetI18NCategory(I18NCat::DIALOG);
|
||||
return di->T("Default");
|
||||
}
|
||||
Path path(*value_);
|
||||
return path.GetFilename();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Common/System/Request.h"
|
||||
|
||||
#include "Common/Data/Text/I18n.h"
|
||||
#include "Common/UI/UIScreen.h"
|
||||
#include "Common/UI/UI.h"
|
||||
@@ -192,7 +194,7 @@ public:
|
||||
void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override;
|
||||
|
||||
void SetPasswordDisplay() {
|
||||
password_ = true;
|
||||
passwordDisplay_ = true;
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -200,7 +202,7 @@ protected:
|
||||
|
||||
float CalculateValueScale(const UIContext &dc, const std::string &valueText, float availWidth) const;
|
||||
|
||||
bool password_ = false;
|
||||
bool passwordDisplay_ = false;
|
||||
};
|
||||
|
||||
// Reads and writes value to determine the current selection.
|
||||
@@ -407,4 +409,20 @@ private:
|
||||
std::string(*translateCallback_)(const char *value) = nullptr;
|
||||
};
|
||||
|
||||
enum class FileChooserFileType {
|
||||
WAVE_FILE,
|
||||
};
|
||||
|
||||
class FileChooserChoice : public AbstractChoiceWithValueDisplay {
|
||||
public:
|
||||
FileChooserChoice(std::string *value, const std::string &title, BrowseFileType fileType, LayoutParams *layoutParams = nullptr);
|
||||
std::string ValueText() const override;
|
||||
|
||||
Event OnChange;
|
||||
|
||||
private:
|
||||
std::string *value_;
|
||||
BrowseFileType fileType_;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
+1
-2
@@ -141,8 +141,7 @@ static void MoveFocus(ViewGroup *root, FocusDirection direction) {
|
||||
return;
|
||||
}
|
||||
|
||||
NeighborResult neigh = root->FindNeighbor(focusedView, direction, neigh);
|
||||
|
||||
NeighborResult neigh = root->FindNeighbor(focusedView, direction, NeighborResult());
|
||||
if (neigh.view) {
|
||||
neigh.view->SetFocus();
|
||||
root->SubviewFocused(neigh.view);
|
||||
|
||||
+6
-1
@@ -265,11 +265,11 @@ static bool DefaultSasThread() {
|
||||
}
|
||||
|
||||
static const ConfigSetting achievementSettings[] = {
|
||||
// Core settings
|
||||
ConfigSetting("AchievementsEnable", &g_Config.bAchievementsEnable, true, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsChallengeMode", &g_Config.bAchievementsChallengeMode, false, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsEncoreMode", &g_Config.bAchievementsEncoreMode, false, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsUnofficial", &g_Config.bAchievementsUnofficial, false, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsSoundEffects", &g_Config.bAchievementsSoundEffects, true, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsLogBadMemReads", &g_Config.bAchievementsLogBadMemReads, false, CfgFlag::DEFAULT),
|
||||
|
||||
// Achievements login info. Note that password is NOT stored, only a login token.
|
||||
@@ -277,6 +277,11 @@ static const ConfigSetting achievementSettings[] = {
|
||||
// from the ini if manually entered (useful when testing various builds on Android).
|
||||
ConfigSetting("AchievementsToken", &g_Config.sAchievementsToken, "", CfgFlag::DONT_SAVE),
|
||||
ConfigSetting("AchievementsUserName", &g_Config.sAchievementsUserName, "", CfgFlag::DEFAULT),
|
||||
|
||||
// Customizations
|
||||
ConfigSetting("AchievementsSoundEffects", &g_Config.bAchievementsSoundEffects, true, CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsUnlockAudioFile", &g_Config.sAchievementsUnlockAudioFile, "", CfgFlag::DEFAULT),
|
||||
ConfigSetting("AchievementsLeaderboardSubmitAudioFile", &g_Config.sAchievementsLeaderboardSubmitAudioFile, "", CfgFlag::DEFAULT),
|
||||
};
|
||||
|
||||
static const ConfigSetting cpuSettings[] = {
|
||||
|
||||
@@ -495,6 +495,10 @@ public:
|
||||
bool bAchievementsSoundEffects;
|
||||
bool bAchievementsLogBadMemReads;
|
||||
|
||||
// Customizations
|
||||
std::string sAchievementsUnlockAudioFile;
|
||||
std::string sAchievementsLeaderboardSubmitAudioFile;
|
||||
|
||||
// Achivements login info. Note that password is NOT stored, only a login token.
|
||||
// Still, we may wanna store it more securely than in PPSSPP.ini, especially on Android.
|
||||
std::string sAchievementsUserName;
|
||||
|
||||
@@ -312,6 +312,9 @@ bool MainUI::HandleCustomEvent(QEvent *e) {
|
||||
case BrowseFileType::DB:
|
||||
filter = "DB files (*.db)";
|
||||
break;
|
||||
case BrowseFileType::SOUND_EFFECT:
|
||||
filter = "WAVE files (*.wav)";
|
||||
break;
|
||||
case BrowseFileType::ANY:
|
||||
break;
|
||||
}
|
||||
|
||||
+2
-1
@@ -244,7 +244,8 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
|
||||
}
|
||||
};
|
||||
DarwinFileSystemServices services;
|
||||
services.presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false);
|
||||
BrowseFileType fileType = (BrowseFileType)param3;
|
||||
services.presentDirectoryPanel(callback, /* allowFiles = */ true, /* allowDirectories = */ false, fileType);
|
||||
return true;
|
||||
}
|
||||
case SystemRequestType::BROWSE_FOR_FOLDER:
|
||||
|
||||
+99
-35
@@ -4,18 +4,20 @@
|
||||
#include "Common/File/VFS/VFS.h"
|
||||
#include "Common/UI/Root.h"
|
||||
|
||||
#include "Common/Data/Text/I18n.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/Data/Format/RIFF.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Common/System/System.h"
|
||||
#include "Common/System/OSD.h"
|
||||
#include "Common/Serialize/SerializeFuncs.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
#include "Common/Data/Collections/FixedSizeQueue.h"
|
||||
#include "Core/HW/SimpleAudioDec.h"
|
||||
#include "Core/HLE/__sceAudio.h"
|
||||
#include "Core/System.h"
|
||||
#include "GameInfoCache.h"
|
||||
#include "Core/Config.h"
|
||||
#include "UI/GameInfoCache.h"
|
||||
#include "UI/BackgroundAudio.h"
|
||||
|
||||
struct WavData {
|
||||
@@ -367,6 +369,44 @@ void BackgroundAudio::Update() {
|
||||
}
|
||||
}
|
||||
|
||||
inline int16_t ConvertU8ToI16(uint8_t value) {
|
||||
int ivalue = value - 128;
|
||||
return ivalue * 255;
|
||||
}
|
||||
|
||||
Sample *Sample::Load(const std::string &path) {
|
||||
size_t bytes;
|
||||
uint8_t *data = g_VFS.ReadFile(path.c_str(), &bytes);
|
||||
if (!data) {
|
||||
WARN_LOG(AUDIO, "Failed to load sample '%s'", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RIFFReader reader(data, (int)bytes);
|
||||
|
||||
WavData wave;
|
||||
wave.Read(reader);
|
||||
|
||||
delete[] data;
|
||||
|
||||
if (wave.num_channels > 2 || wave.raw_bytes_per_frame > sizeof(int16_t) * wave.num_channels) {
|
||||
ERROR_LOG(AUDIO, "Wave format not supported for mixer playback. Must be 8-bit or 16-bit raw mono or stereo. '%s'", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int16_t *samples = new int16_t[wave.num_channels * wave.numFrames];
|
||||
if (wave.raw_bytes_per_frame == wave.num_channels * 2) {
|
||||
// 16-bit
|
||||
memcpy(samples, wave.raw_data, wave.numFrames * wave.raw_bytes_per_frame);
|
||||
} else if (wave.raw_bytes_per_frame == wave.num_channels) {
|
||||
// 8-bit. Convert.
|
||||
for (int i = 0; i < wave.num_channels * wave.numFrames; i++) {
|
||||
samples[i] = ConvertU8ToI16(wave.raw_data[i]);
|
||||
}
|
||||
}
|
||||
return new Sample(samples, wave.num_channels, wave.numFrames, wave.sample_rate);
|
||||
}
|
||||
|
||||
static inline int16_t Clamp16(int32_t sample) {
|
||||
if (sample < -32767) return -32767;
|
||||
if (sample > 32767) return 32767;
|
||||
@@ -407,15 +447,25 @@ void SoundEffectMixer::Mix(int16_t *buffer, int sz, int sampleRateHz) {
|
||||
int wholeOffset = iter->offset >> 32;
|
||||
int frac = (iter->offset >> 20) & 0xFFF; // Use a 12 bit fraction to get away with 32-bit multiplies
|
||||
|
||||
int interpolatedLeft = (sample->data_[wholeOffset * 2] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2] * frac) >> 12;
|
||||
int interpolatedRight = (sample->data_[wholeOffset * 2 + 1] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2 + 1] * frac) >> 12;
|
||||
if (sample->channels_ == 2) {
|
||||
int interpolatedLeft = (sample->data_[wholeOffset * 2] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2] * frac) >> 12;
|
||||
int interpolatedRight = (sample->data_[wholeOffset * 2 + 1] * (0x1000 - frac) + sample->data_[(wholeOffset + 1) * 2 + 1] * frac) >> 12;
|
||||
|
||||
// Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well.
|
||||
int left = Clamp16(buffer[i] + (interpolatedLeft * iter->volume >> 8));
|
||||
int right = Clamp16(buffer[i + 1] + (interpolatedRight * iter->volume >> 8));
|
||||
// Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well.
|
||||
int left = Clamp16(buffer[i] + (interpolatedLeft * iter->volume >> 8));
|
||||
int right = Clamp16(buffer[i + 1] + (interpolatedRight * iter->volume >> 8));
|
||||
|
||||
buffer[i] = left;
|
||||
buffer[i + 1] = right;
|
||||
buffer[i] = left;
|
||||
buffer[i + 1] = right;
|
||||
} else if (sample->channels_ == 1) {
|
||||
int interpolated = (sample->data_[wholeOffset] * (0x1000 - frac) + sample->data_[wholeOffset + 1] * frac) >> 12;
|
||||
|
||||
// Clamping add on top per sample. Not great, we should be mixing at higher bitrate instead. Oh well.
|
||||
int value = Clamp16(buffer[i] + (interpolated * iter->volume >> 8));
|
||||
|
||||
buffer[i] = value;
|
||||
buffer[i + 1] = value;
|
||||
}
|
||||
|
||||
iter->offset += stride;
|
||||
}
|
||||
@@ -433,40 +483,54 @@ void SoundEffectMixer::Play(UI::UISound sfx, float volume) {
|
||||
queue_.push_back(PlayInstance{ sfx, 0, (int)(255.0f * volume), false });
|
||||
}
|
||||
|
||||
Sample *SoundEffectMixer::LoadSample(const std::string &path) {
|
||||
size_t bytes;
|
||||
uint8_t *data = g_VFS.ReadFile(path.c_str(), &bytes);
|
||||
if (!data) {
|
||||
WARN_LOG(AUDIO, "Failed to load sample '%s'", path.c_str());
|
||||
return nullptr;
|
||||
void SoundEffectMixer::UpdateSample(UI::UISound sound, Sample *sample) {
|
||||
if (sample) {
|
||||
std::lock_guard<std::mutex> guard(mutex_);
|
||||
samples_[(size_t)sound] = std::unique_ptr<Sample>(sample);
|
||||
} else {
|
||||
LoadDefaultSample(sound);
|
||||
}
|
||||
}
|
||||
|
||||
RIFFReader reader(data, (int)bytes);
|
||||
|
||||
WavData wave;
|
||||
wave.Read(reader);
|
||||
|
||||
delete[] data;
|
||||
|
||||
if (wave.num_channels != 2 || wave.raw_bytes_per_frame != 4) {
|
||||
ERROR_LOG(AUDIO, "Wave format not supported for mixer playback. Must be 16-bit raw stereo. '%s'", path.c_str());
|
||||
return nullptr;
|
||||
void SoundEffectMixer::LoadDefaultSample(UI::UISound sound) {
|
||||
const char *filename = nullptr;
|
||||
switch (sound) {
|
||||
case UI::UISound::BACK: filename = "sfx_back.wav"; break;
|
||||
case UI::UISound::SELECT: filename = "sfx_select.wav"; break;
|
||||
case UI::UISound::CONFIRM: filename = "sfx_confirm.wav"; break;
|
||||
case UI::UISound::TOGGLE_ON: filename = "sfx_toggle_on.wav"; break;
|
||||
case UI::UISound::TOGGLE_OFF: filename = "sfx_toggle_off.wav"; break;
|
||||
case UI::UISound::ACHIEVEMENT_UNLOCKED: filename = "sfx_achievement_unlocked.wav"; break;
|
||||
case UI::UISound::LEADERBOARD_SUBMITTED: filename = "sfx_leaderbord_submitted.wav"; break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
int16_t *samples = new int16_t[2 * wave.numFrames];
|
||||
memcpy(samples, wave.raw_data, wave.numFrames * wave.raw_bytes_per_frame);
|
||||
return new Sample(samples, wave.numFrames, wave.sample_rate);
|
||||
Sample *sample = Sample::Load(filename);
|
||||
if (!sample) {
|
||||
ERROR_LOG(SYSTEM, "Failed to load the default sample for UI sound %d", (int)sound);
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(mutex_);
|
||||
samples_[(size_t)sound] = std::unique_ptr<Sample>(sample);
|
||||
}
|
||||
|
||||
void SoundEffectMixer::LoadSamples() {
|
||||
samples_.resize((size_t)UI::UISound::COUNT);
|
||||
samples_[(size_t)UI::UISound::BACK] = std::unique_ptr<Sample>(LoadSample("sfx_back.wav"));
|
||||
samples_[(size_t)UI::UISound::SELECT] = std::unique_ptr<Sample>(LoadSample("sfx_select.wav"));
|
||||
samples_[(size_t)UI::UISound::CONFIRM] = std::unique_ptr<Sample>(LoadSample("sfx_confirm.wav"));
|
||||
samples_[(size_t)UI::UISound::TOGGLE_ON] = std::unique_ptr<Sample>(LoadSample("sfx_toggle_on.wav"));
|
||||
samples_[(size_t)UI::UISound::TOGGLE_OFF] = std::unique_ptr<Sample>(LoadSample("sfx_toggle_off.wav"));
|
||||
samples_[(size_t)UI::UISound::ACHIEVEMENT_UNLOCKED] = std::unique_ptr<Sample>(LoadSample("sfx_achievement_unlocked.wav"));
|
||||
samples_[(size_t)UI::UISound::LEADERBOARD_SUBMITTED] = std::unique_ptr<Sample>(LoadSample("sfx_leaderbord_submitted.wav"));
|
||||
LoadDefaultSample(UI::UISound::BACK);
|
||||
LoadDefaultSample(UI::UISound::SELECT);
|
||||
LoadDefaultSample(UI::UISound::CONFIRM);
|
||||
LoadDefaultSample(UI::UISound::TOGGLE_ON);
|
||||
LoadDefaultSample(UI::UISound::TOGGLE_OFF);
|
||||
|
||||
if (!g_Config.sAchievementsUnlockAudioFile.empty()) {
|
||||
UpdateSample(UI::UISound::ACHIEVEMENT_UNLOCKED, Sample::Load(g_Config.sAchievementsUnlockAudioFile));
|
||||
} else {
|
||||
LoadDefaultSample(UI::UISound::ACHIEVEMENT_UNLOCKED);
|
||||
}
|
||||
if (!g_Config.sAchievementsLeaderboardSubmitAudioFile.empty()) {
|
||||
UpdateSample(UI::UISound::LEADERBOARD_SUBMITTED, Sample::Load(g_Config.sAchievementsLeaderboardSubmitAudioFile));
|
||||
} else {
|
||||
LoadDefaultSample(UI::UISound::LEADERBOARD_SUBMITTED);
|
||||
}
|
||||
|
||||
UI::SetSoundCallback([](UI::UISound sound, float volume) {
|
||||
g_BackgroundAudio.SFX().Play(sound, volume);
|
||||
|
||||
@@ -12,24 +12,29 @@ class AT3PlusReader;
|
||||
|
||||
struct Sample {
|
||||
// data must be new-ed.
|
||||
Sample(int16_t *data, int length, int rateInHz) : data_(data), length_(length), rateInHz_(rateInHz) {}
|
||||
Sample(int16_t *data, int channels, int length, int rateInHz) : channels_(channels), data_(data), length_(length), rateInHz_(rateInHz) {}
|
||||
~Sample() {
|
||||
delete[] data_;
|
||||
}
|
||||
int16_t *data_;
|
||||
int length_; // stereo samples.
|
||||
int length_; // stereo or mono samples.
|
||||
int rateInHz_; // sampleRate
|
||||
int channels_;
|
||||
|
||||
static Sample *Load(const std::string &path);
|
||||
};
|
||||
|
||||
// Mixer for things played on top of everything.
|
||||
class SoundEffectMixer {
|
||||
public:
|
||||
static Sample *LoadSample(const std::string &path);
|
||||
void LoadSamples();
|
||||
|
||||
void Mix(int16_t *buffer, int sz, int sampleRateHz);
|
||||
void Play(UI::UISound sfx, float volume);
|
||||
|
||||
void UpdateSample(UI::UISound sound, Sample *sample);
|
||||
void LoadDefaultSample(UI::UISound sound);
|
||||
|
||||
std::vector<std::unique_ptr<Sample>> samples_;
|
||||
|
||||
struct PlayInstance {
|
||||
@@ -39,6 +44,7 @@ public:
|
||||
bool done;
|
||||
};
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
std::vector<PlayInstance> queue_;
|
||||
std::vector<PlayInstance> plays_;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "ppsspp_config.h"
|
||||
#include "Common/File/Path.h"
|
||||
#include "Common/System/Request.h"
|
||||
|
||||
#define PreferredMemoryStickUserDefaultsKey "UserPreferredMemoryStickDirectoryPath"
|
||||
|
||||
@@ -18,18 +19,20 @@ typedef std::function<void (bool, Path)> DarwinDirectoryPanelCallback;
|
||||
/// on Darwin platforms.
|
||||
class DarwinFileSystemServices {
|
||||
public:
|
||||
/// Present a pannel to choose the directory as the memory stick manager.
|
||||
void presentDirectoryPanel(DarwinDirectoryPanelCallback,
|
||||
bool allowFiles = false,
|
||||
bool allowDirectories = true);
|
||||
|
||||
static Path appropriateMemoryStickDirectoryToUse();
|
||||
static void setUserPreferredMemoryStickDirectory(Path);
|
||||
/// Present a panel to choose a file or directory.
|
||||
void presentDirectoryPanel(
|
||||
DarwinDirectoryPanelCallback,
|
||||
bool allowFiles = false,
|
||||
bool allowDirectories = true,
|
||||
BrowseFileType fileType = BrowseFileType::ANY);
|
||||
|
||||
static Path appropriateMemoryStickDirectoryToUse();
|
||||
static void setUserPreferredMemoryStickDirectory(Path);
|
||||
private:
|
||||
static Path __defaultMemoryStickPath();
|
||||
static Path __defaultMemoryStickPath();
|
||||
#if PPSSPP_PLATFORM(IOS)
|
||||
// iOS only, needed for UIDocumentPickerViewController
|
||||
void *__pickerDelegate = NULL;
|
||||
// iOS only, needed for UIDocumentPickerViewController
|
||||
void *__pickerDelegate = NULL;
|
||||
#endif // PPSSPP_PLATFORM(IOS)
|
||||
};
|
||||
|
||||
|
||||
@@ -44,15 +44,35 @@
|
||||
#include <AppKit/AppKit.h>
|
||||
#endif // __has_include(<UIKit/UIKit.h>)
|
||||
|
||||
void DarwinFileSystemServices::presentDirectoryPanel(DarwinDirectoryPanelCallback callback,
|
||||
bool allowFiles,
|
||||
bool allowDirectories) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
void DarwinFileSystemServices::presentDirectoryPanel(
|
||||
DarwinDirectoryPanelCallback callback,
|
||||
bool allowFiles, bool allowDirectories,
|
||||
BrowseFileType fileType) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
#if PPSSPP_PLATFORM(MAC)
|
||||
NSOpenPanel *panel = [[NSOpenPanel alloc] init];
|
||||
panel.allowsMultipleSelection = NO;
|
||||
panel.canChooseFiles = allowFiles;
|
||||
panel.canChooseDirectories = allowDirectories;
|
||||
NSOpenPanel *panel = [[NSOpenPanel alloc] init];
|
||||
panel.allowsMultipleSelection = NO;
|
||||
panel.canChooseFiles = allowFiles;
|
||||
panel.canChooseDirectories = allowDirectories;
|
||||
switch (fileType) {
|
||||
case BrowseFileType::BOOTABLE:
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObjects:@"iso", @"cso", @"pbp", @"elf", @"zip", @"ppdmp", nil]];
|
||||
break;
|
||||
case BrowseFileType::IMAGE:
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObjects:@"jpg", @"png", nil]];
|
||||
break;
|
||||
case BrowseFileType::INI:
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObject:@"ini"]];
|
||||
break;
|
||||
case BrowseFileType::DB:
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObject:@"db"]];
|
||||
break;
|
||||
case BrowseFileType::SOUND_EFFECT:
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObject:@"wav"]];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// if (!allowFiles && allowDirectories)
|
||||
// panel.allowedFileTypes = @[(__bridge NSString *)kUTTypeFolder];
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#include "UI/RetroAchievementScreens.h"
|
||||
#include "Common/System/OSD.h"
|
||||
#include "Common/System/Request.h"
|
||||
#include "Common/UI/View.h"
|
||||
@@ -10,10 +9,57 @@
|
||||
#include "Core/Config.h"
|
||||
#include "Core/RetroAchievements.h"
|
||||
|
||||
#include "UI/RetroAchievementScreens.h"
|
||||
#include "UI/BackgroundAudio.h"
|
||||
|
||||
static inline const char *DeNull(const char *ptr) {
|
||||
return ptr ? ptr : "";
|
||||
}
|
||||
|
||||
// Compound view, creating a FileChooserChoice inside.
|
||||
class AudioFileChooser : public UI::LinearLayout {
|
||||
public:
|
||||
AudioFileChooser(std::string *value, const std::string &title, UI::UISound sound, UI::LayoutParams *layoutParams = nullptr);
|
||||
|
||||
UI::UISound sound_;
|
||||
};
|
||||
|
||||
static constexpr UI::Size ITEM_HEIGHT = 64.f;
|
||||
|
||||
AudioFileChooser::AudioFileChooser(std::string *value, const std::string &title, UI::UISound sound, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams), sound_(sound) {
|
||||
using namespace UI;
|
||||
SetSpacing(2.0f);
|
||||
if (!layoutParams) {
|
||||
layoutParams_->width = FILL_PARENT;
|
||||
layoutParams_->height = ITEM_HEIGHT;
|
||||
}
|
||||
Add(new FileChooserChoice(value, title, BrowseFileType::SOUND_EFFECT, new LinearLayoutParams(1.0f)))->OnChange.Add([=](UI::EventParams &e) {
|
||||
// TODO: Check the file format here.
|
||||
// Need to forward the event out.
|
||||
std::string path = e.s;
|
||||
Sample *sample = Sample::Load(path);
|
||||
if (sample) {
|
||||
g_BackgroundAudio.SFX().UpdateSample(sound, sample);
|
||||
} else {
|
||||
if (!sample) {
|
||||
auto au = GetI18NCategory(I18NCat::AUDIO);
|
||||
g_OSD.Show(OSDType::MESSAGE_WARNING, au->T("Audio file format not supported. Must be 16-bit WAV."));
|
||||
}
|
||||
value->clear();
|
||||
}
|
||||
return UI::EVENT_DONE;
|
||||
});
|
||||
Add(new Choice(ImageID("I_ARROW_RIGHT"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([=](UI::EventParams &) {
|
||||
g_BackgroundAudio.SFX().Play(sound_, 0.6f);
|
||||
return UI::EVENT_DONE;
|
||||
});
|
||||
Add(new Choice(ImageID("I_TRASHCAN"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)))->OnClick.Add([=](UI::EventParams &) {
|
||||
g_BackgroundAudio.SFX().UpdateSample(sound, nullptr);
|
||||
value->clear();
|
||||
return UI::EVENT_DONE;
|
||||
});
|
||||
}
|
||||
|
||||
void RetroAchievementsListScreen::CreateTabs() {
|
||||
auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS);
|
||||
|
||||
@@ -194,6 +240,10 @@ void RetroAchievementsSettingsScreen::CreateTabs() {
|
||||
using namespace UI;
|
||||
|
||||
CreateAccountTab(AddTab("AchievementsAccount", ac->T("Account")));
|
||||
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
|
||||
// Don't bother creating this tab if we don't have a file browser.
|
||||
CreateCustomizeTab(AddTab("AchievementsCustomize", ac->T("Customize")));
|
||||
}
|
||||
CreateDeveloperToolsTab(AddTab("AchievementsDeveloperTools", sy->T("Developer Tools")));
|
||||
}
|
||||
|
||||
@@ -283,6 +333,15 @@ void RetroAchievementsSettingsScreen::CreateAccountTab(UI::ViewGroup *viewGroup)
|
||||
});
|
||||
}
|
||||
|
||||
void RetroAchievementsSettingsScreen::CreateCustomizeTab(UI::ViewGroup *viewGroup) {
|
||||
auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS);
|
||||
|
||||
using namespace UI;
|
||||
viewGroup->Add(new ItemHeader(ac->T("Sound effects")));
|
||||
viewGroup->Add(new AudioFileChooser(&g_Config.sAchievementsUnlockAudioFile, "Achievement unlocked", UISound::ACHIEVEMENT_UNLOCKED));
|
||||
viewGroup->Add(new AudioFileChooser(&g_Config.sAchievementsLeaderboardSubmitAudioFile, "Leaderboard score submission", UISound::LEADERBOARD_SUBMITTED));
|
||||
}
|
||||
|
||||
void RetroAchievementsSettingsScreen::CreateDeveloperToolsTab(UI::ViewGroup *viewGroup) {
|
||||
auto ac = GetI18NCategory(I18NCat::ACHIEVEMENTS);
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ protected:
|
||||
|
||||
private:
|
||||
void CreateAccountTab(UI::ViewGroup *viewGroup);
|
||||
void CreateCustomizeTab(UI::ViewGroup *viewGroup);
|
||||
void CreateDeveloperToolsTab(UI::ViewGroup *viewGroup);
|
||||
|
||||
std::string username_;
|
||||
|
||||
@@ -488,6 +488,9 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
|
||||
case BrowseFileType::DB:
|
||||
picker->FileTypeFilter->Append(".db");
|
||||
break;
|
||||
case BrowseFileType::SOUND_EFFECT:
|
||||
picker->FileTypeFilter->Append(".wav");
|
||||
break;
|
||||
case BrowseFileType::ANY:
|
||||
picker->FileTypeFilter->Append("*");
|
||||
break;
|
||||
|
||||
@@ -537,6 +537,9 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
|
||||
case BrowseFileType::DB:
|
||||
filter = MakeFilter(L"Cheat db files (*.db)|*.db|All files (*.*)|*.*||");
|
||||
break;
|
||||
case BrowseFileType::SOUND_EFFECT:
|
||||
filter = MakeFilter(L"WAVE files (*.wav)|*.wav|All files (*.*)|*.*||");
|
||||
break;
|
||||
case BrowseFileType::ANY:
|
||||
filter = MakeFilter(L"All files (*.*)|*.*||");
|
||||
break;
|
||||
|
||||
@@ -1066,8 +1066,18 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
|
||||
PushCommand("browse_image", StringFromFormat("%d", requestId));
|
||||
return true;
|
||||
case SystemRequestType::BROWSE_FOR_FILE:
|
||||
PushCommand("browse_file", StringFromFormat("%d", requestId));
|
||||
{
|
||||
BrowseFileType fileType = (BrowseFileType)param3;
|
||||
switch (fileType) {
|
||||
case BrowseFileType::SOUND_EFFECT:
|
||||
PushCommand("browse_file_audio", StringFromFormat("%d", requestId));
|
||||
break;
|
||||
default:
|
||||
PushCommand("browse_file", StringFromFormat("%d", requestId));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case SystemRequestType::BROWSE_FOR_FOLDER:
|
||||
PushCommand("browse_folder", StringFromFormat("%d", requestId));
|
||||
return true;
|
||||
|
||||
@@ -1373,14 +1373,18 @@ public abstract class NativeActivity extends Activity {
|
||||
Log.e(TAG, e.toString());
|
||||
return false;
|
||||
}
|
||||
} else if (command.equals("browse_file")) {
|
||||
} else if (command.equals("browse_file") || command.equals("browse_file_audio")) {
|
||||
try {
|
||||
int requestId = Integer.parseInt(params);
|
||||
int packedResultCode = packResultCode(RESULT_OPEN_DOCUMENT, requestId);
|
||||
Log.i(TAG, "browse_file request ID: " + requestId + " packed: " + packedResultCode);
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
if (command.equals("browse_file_audio")) {
|
||||
intent.setType("audio/x-wav");
|
||||
} else {
|
||||
intent.setType("*/*");
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||
// Possible alternative approach:
|
||||
// String[] mimeTypes = {"application/octet-stream", "/x-iso9660-image"};
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
%1: Leaderboard attempt failed = %1: Topplisteförsök misslyckades
|
||||
%1: Submitting leaderboard score: %2! = %1: Skickar in toppliste-poäng: %2!
|
||||
Account = Inloggning
|
||||
Achievement Unlocked = Achievement Unlocked
|
||||
Achievements = Achievements
|
||||
Challenge Mode = Utmanings-läge
|
||||
Challenge Mode (no savestates) = Utmanings-läge (inga sparade state)
|
||||
Customize = Customize
|
||||
Earned = Du har tjänat %d av %d achievements, och %d of %d poäng
|
||||
Failed to log in, check your username and password. = Misslyckades att logga in, kontrollera ditt användarnamn och lösenord.
|
||||
How to use RetroAchievements = Hur man använder RetroAchievements
|
||||
Leaderboard score submission = Leaderboard score submission
|
||||
Leaderboard submission is enabled = Skickar in poäng till ledartabeller
|
||||
Leaderboards = Leaderboards
|
||||
Links = Links
|
||||
@@ -317,6 +320,7 @@ ConnectingAP = Kopplar upp till acceesspunkten.\nVänta...
|
||||
ConnectingPleaseWait = Kopplar upp.\nVänta...
|
||||
ConnectionName = Uppkopplingsnamn
|
||||
Corrupted Data = Korrupt data
|
||||
Default = Default
|
||||
Delete = Radera
|
||||
Delete all = Radera allt
|
||||
Delete completed = Borttaget.
|
||||
|
||||
+1
-1
Submodule ext/rcheevos updated: 73f924f08d...223df07613.
Reference in new issue
Block a user