diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index 02841f87e5..f6f2dde904 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -29,6 +29,8 @@ bool NativeIsAtTopLevel(); // before this, although it may be called at any point in time afterwards (on any thread!) // This functions must NOT call OpenGL. Main thread. void NativeInit(int argc, const char *argv[], const char *savegame_dir, const char *external_dir, const char *cache_dir); +void NativeSetAchievementsHostOverride(std::string_view host); +void NativeClearAchievementsHostOverride(); // Runs after NativeInit() at some point. May (and probably should) call OpenGL. // Should not initialize anything screen-size-dependent - do that in NativeResized. diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index ab3b410d3d..8746b37a30 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -25,6 +25,8 @@ #include "ppsspp_config.h" +#include "ext/rcheevos/include/rc_client.h" + // Background worker threads should be spawned in NativeInit and joined // in NativeShutdown. #include @@ -191,6 +193,71 @@ static Draw::Pipeline *texColorPipeline; static UIContext *uiContext; static int g_restartGraphics; static bool g_windowHidden = false; +static std::string g_achievementsHostOverride; +static std::string g_savedAchievementsHost; +static bool g_savedAchievementsHardcoreMode = false; +static bool g_hasSavedAchievementsSettings = false; +static bool g_nativeMainThreadReady = false; + +static void ApplyAchievementsRuntimeSettings() { + auto *client = Achievements::GetClient(); + if (!client) { + return; + } + + if (!g_Config.sAchievementsHost.empty()) { + rc_client_set_host(client, g_Config.sAchievementsHost.c_str()); + } else if (!System_GetPropertyBool(SYSPROP_SUPPORTS_HTTPS)) { + rc_client_set_host(client, "http://retroachievements.org"); + } else { + rc_client_set_host(client, "https://retroachievements.org"); + } + + rc_client_set_hardcore_enabled(client, g_Config.bAchievementsHardcoreMode ? 1 : 0); +} + +static void ApplyAchievementsHostOverride() { + if (g_achievementsHostOverride.empty()) { + return; + } + + if (!g_hasSavedAchievementsSettings) { + g_savedAchievementsHost = g_Config.sAchievementsHost; + g_savedAchievementsHardcoreMode = g_Config.bAchievementsHardcoreMode; + g_hasSavedAchievementsSettings = true; + } + + g_Config.DoNotSaveSetting(&g_Config.sAchievementsHost); + g_Config.sAchievementsHost = g_achievementsHostOverride; + g_Config.DoNotSaveSetting(&g_Config.bAchievementsHardcoreMode); + g_Config.bAchievementsHardcoreMode = false; + ApplyAchievementsRuntimeSettings(); +} + +static void ClearAchievementsHostOverride() { + g_achievementsHostOverride.clear(); + if (!g_hasSavedAchievementsSettings) { + return; + } + + g_Config.DoNotSaveSetting(&g_Config.sAchievementsHost); + g_Config.sAchievementsHost = g_savedAchievementsHost; + g_Config.DoNotSaveSetting(&g_Config.bAchievementsHardcoreMode); + g_Config.bAchievementsHardcoreMode = g_savedAchievementsHardcoreMode; + g_savedAchievementsHost.clear(); + g_savedAchievementsHardcoreMode = false; + g_hasSavedAchievementsSettings = false; + ApplyAchievementsRuntimeSettings(); +} + +static void RunAchievementsOverrideUpdate(std::function func) { + if (g_nativeMainThreadReady) { + System_RunOnMainThread(std::move(func)); + } else { + func(); + } +} + std::vector> g_pendingClosures; AudioBackend *g_audioBackend = nullptr; @@ -707,6 +774,8 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_DownloadManager.SetCacheDir(GetSysDirectory(DIRECTORY_APP_CACHE)); + ApplyAchievementsHostOverride(); + DEBUG_LOG(Log::System, "ScreenManager!"); g_screenManager = new ScreenManager(); if (g_Config.memStickDirectory.empty()) { @@ -756,6 +825,21 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch // Must be done restarting by now. restarting = false; + g_nativeMainThreadReady = true; +} + +void NativeSetAchievementsHostOverride(std::string_view host) { + std::string hostCopy(host); + RunAchievementsOverrideUpdate([host = std::move(hostCopy)] { + g_achievementsHostOverride = host; + ApplyAchievementsHostOverride(); + }); +} + +void NativeClearAchievementsHostOverride() { + RunAchievementsOverrideUpdate([] { + ClearAchievementsHostOverride(); + }); } void CallbackPostRender(UIContext *dc, void *userdata); @@ -1470,6 +1554,8 @@ bool NativeIsRestarting() { void NativeShutdown() { INFO_LOG(Log::System, "NativeShutdown begin"); + ClearAchievementsHostOverride(); + g_nativeMainThreadReady = false; Achievements::Shutdown(); diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 8bdf525a19..5ffcdaab7f 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -116,6 +116,14 @@ + + + + + + diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 84daae7224..f472a9e403 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -870,6 +870,15 @@ retry: } } +extern "C" void Java_org_ppsspp_ppsspp_NativeApp_setAchievementsHostOverride(JNIEnv *env, jclass, jstring jhost) { + std::string host = GetJavaString(env, jhost); + NativeSetAchievementsHostOverride(host); +} + +extern "C" void Java_org_ppsspp_ppsspp_NativeApp_clearAchievementsHostOverride(JNIEnv *, jclass) { + NativeClearAchievementsHostOverride(); +} + AudioBackend *System_CreateAudioBackend() { // Use legacy mechanisms. return nullptr; diff --git a/android/src/org/ppsspp/ppsspp/AchievementsHostOverrideReceiver.java b/android/src/org/ppsspp/ppsspp/AchievementsHostOverrideReceiver.java new file mode 100644 index 0000000000..8a51a60dc5 --- /dev/null +++ b/android/src/org/ppsspp/ppsspp/AchievementsHostOverrideReceiver.java @@ -0,0 +1,109 @@ +package org.ppsspp.ppsspp; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +public class AchievementsHostOverrideReceiver extends BroadcastReceiver { + private static final String TAG = "AchievementsHostOverride"; + public static final String ACTION_SET = "org.ppsspp.ppsspp.action.SET_ACHIEVEMENTS_HOST_OVERRIDE"; + public static final String ACTION_CLEAR = "org.ppsspp.ppsspp.action.CLEAR_ACHIEVEMENTS_HOST_OVERRIDE"; + public static final String EXTRA_HOST = "host"; + private static final String PREFS_NAME = "achievements_host_override"; + private static final String KEY_HOST = "host"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || intent.getAction() == null) { + return; + } + + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + String action = intent.getAction(); + + if (ACTION_CLEAR.equals(action)) { + clearHostOverride(prefs); + PpssppActivity.clearAchievementsHostOverride(); + Log.i(TAG, "Cleared achievements host override"); + return; + } + + if (!ACTION_SET.equals(action)) { + return; + } + + String host = intent.getStringExtra(EXTRA_HOST); + if (host != null) { + host = host.trim(); + } + + if (TextUtils.isEmpty(host)) { + clearHostOverride(prefs); + PpssppActivity.clearAchievementsHostOverride(); + Log.i(TAG, "Empty host received, cleared override"); + return; + } + + if (!isAllowedHost(host)) { + Log.w(TAG, "Rejected non-loopback achievements host override"); + return; + } + + prefs.edit().putString(KEY_HOST, host).apply(); + PpssppActivity.applyAchievementsHostOverride(host); + Log.i(TAG, "Stored achievements host override"); + } + + public static String getAchievementsHostOverride(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getString(KEY_HOST, null); + } + + private static void clearHostOverride(SharedPreferences prefs) { + prefs.edit().remove(KEY_HOST).apply(); + } + + private static boolean isAllowedHost(String host) { + Uri uri = Uri.parse(host); + if (uri == null) { + return false; + } + + if (!"http".equals(uri.getScheme())) { + return false; + } + + if (!uri.isHierarchical()) { + return false; + } + + String authority = uri.getEncodedAuthority(); + if (TextUtils.isEmpty(authority) || authority.contains("@")) { + return false; + } + + if (!TextUtils.isEmpty(uri.getEncodedPath()) && !"/".equals(uri.getEncodedPath())) { + return false; + } + + if (!TextUtils.isEmpty(uri.getEncodedQuery()) || !TextUtils.isEmpty(uri.getEncodedFragment())) { + return false; + } + + int port = uri.getPort(); + if (port < 0 || port > 65535) { + return false; + } + + String parsedHost = uri.getHost(); + if (TextUtils.isEmpty(parsedHost)) { + return false; + } + + return "127.0.0.1".equals(parsedHost) || "localhost".equalsIgnoreCase(parsedHost) || "::1".equals(parsedHost); + } +} diff --git a/android/src/org/ppsspp/ppsspp/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index d3fe003379..6dc3c42433 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -71,6 +71,8 @@ public class NativeApp { public static native void mouseDelta(float x, float y); public static native void sendMessageFromJava(String msg, String arg); public static native void sendRequestResult(int seqID, boolean result, String value, int iValue); + public static native void setAchievementsHostOverride(String host); + public static native void clearAchievementsHostOverride(); public static native String queryConfig(String queryName); public static native int getSelectedCamera(); diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 55ada7d424..1a581df902 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -172,6 +172,22 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList public static boolean libraryLoaded = false; + public static void applyAchievementsHostOverride(String host) { + if (!initialized || !libraryLoaded || host == null || host.isEmpty()) { + return; + } + + NativeApp.setAchievementsHostOverride(host); + } + + public static void clearAchievementsHostOverride() { + if (!initialized || !libraryLoaded) { + return; + } + + NativeApp.clearAchievementsHostOverride(); + } + public static void CheckABIAndLoadLibrary() { try { System.loadLibrary("ppsspp_jni"); @@ -707,6 +723,12 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList setShortcutParam(shortcutParam); } + String achievementsHostOverride = AchievementsHostOverrideReceiver.getAchievementsHostOverride(this); + if (achievementsHostOverride != null && !achievementsHostOverride.isEmpty()) { + NativeApp.setAchievementsHostOverride(achievementsHostOverride); + Log.i(TAG, "Found achievements host override"); + } + lifeCycle.onCreate(); mSensorManager = (SensorManager)getSystemService(Activity.SENSOR_SERVICE);