Added RetroAchievements!

This commit is contained in:
izzy2lost
2025-10-30 02:41:52 -04:00
parent cd9eddf25c
commit 47df7578f8
23 changed files with 1801 additions and 0 deletions
+7
View File
@@ -28,3 +28,10 @@ MEMORY_CARD_GUIDE.md
MEMCARD_QUICK_REFERENCE.md
MEMCARD_FEATURE_SUMMARY.md
MEMCARD_ARCHITECTURE.md
# Ignore all Markdown and txt files
*.md
*.txt
# But do NOT ignore README.md
!README.md
+128
View File
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: 2025 Android Port Contributors
// SPDX-License-Identifier: GPL-3.0+
#include "AchievementsAndroid.h"
#include "AchievementsJNI.h"
#include "pcsx2/Achievements.h"
#include "common/Console.h"
#include "rc_client.h"
namespace AchievementsAndroid
{
static bool s_initialized = false;
bool Initialize()
{
if (s_initialized)
return true;
Console.WriteLn("AchievementsAndroid: Initializing...");
s_initialized = true;
return true;
}
void Shutdown()
{
if (!s_initialized)
return;
Console.WriteLn("AchievementsAndroid: Shutting down...");
s_initialized = false;
}
void NotifyAchievementUnlocked(const rc_client_achievement_t* achievement)
{
if (!s_initialized || !achievement)
return;
const bool is_hardcore = Achievements::IsHardcoreModeActive();
AchievementsJNI::OnAchievementUnlocked(
achievement->title,
achievement->description,
achievement->points,
is_hardcore
);
}
void NotifyGameComplete(const char* game_title, int achievement_count, int total_points)
{
if (!s_initialized)
return;
AchievementsJNI::OnGameComplete(game_title, achievement_count, total_points);
}
void NotifyLeaderboardStarted(const rc_client_leaderboard_t* leaderboard)
{
if (!s_initialized || !leaderboard)
return;
AchievementsJNI::OnLeaderboardStarted(leaderboard->title);
}
void NotifyLeaderboardSubmitted(const rc_client_leaderboard_t* leaderboard,
const char* score, int rank, int total_entries)
{
if (!s_initialized || !leaderboard)
return;
AchievementsJNI::OnLeaderboardSubmitted(
leaderboard->title,
score ? score : "Unknown",
rank,
total_entries
);
}
void NotifyLoginSuccess(const char* username, int score, int softcore_score, int unread_messages)
{
if (!s_initialized)
return;
AchievementsJNI::OnLoginSuccess(username, score, softcore_score, unread_messages);
}
void NotifyChallengeIndicatorShow(const rc_client_achievement_t* achievement)
{
if (!s_initialized || !achievement)
return;
AchievementsJNI::OnChallengeIndicatorShow(achievement->title);
}
void NotifyProgressIndicatorUpdate(const rc_client_achievement_t* achievement)
{
if (!s_initialized || !achievement)
return;
const char* progress = achievement->measured_progress[0] ? achievement->measured_progress : "0%";
AchievementsJNI::OnProgressIndicatorUpdate(achievement->title, progress);
}
void NotifyGameSummary(const char* game_title, const rc_client_user_game_summary_t* summary)
{
if (!s_initialized || !summary)
return;
const bool is_hardcore = Achievements::IsHardcoreModeActive();
AchievementsJNI::OnGameSummary(
game_title,
summary->num_unlocked_achievements,
summary->num_core_achievements,
summary->points_unlocked,
summary->points_core,
is_hardcore
);
}
void ShowNotification(const char* message, bool is_long)
{
if (!s_initialized)
return;
AchievementsJNI::ShowNotification(message, is_long ? 1 : 0);
}
} // namespace AchievementsAndroid
+47
View File
@@ -0,0 +1,47 @@
// SPDX-FileCopyrightText: 2025 Android Port Contributors
// SPDX-License-Identifier: GPL-3.0+
#pragma once
// Forward declarations from rc_client.h
struct rc_client_achievement_t;
struct rc_client_leaderboard_t;
struct rc_client_user_game_summary_t;
namespace AchievementsAndroid
{
/// Initialize the Android achievements system
bool Initialize();
/// Shutdown the Android achievements system
void Shutdown();
/// Notify that an achievement was unlocked
void NotifyAchievementUnlocked(const rc_client_achievement_t* achievement);
/// Notify that the game was completed
void NotifyGameComplete(const char* game_title, int achievement_count, int total_points);
/// Notify that a leaderboard attempt started
void NotifyLeaderboardStarted(const rc_client_leaderboard_t* leaderboard);
/// Notify that a leaderboard score was submitted
void NotifyLeaderboardSubmitted(const rc_client_leaderboard_t* leaderboard,
const char* score, int rank, int total_entries);
/// Notify that login was successful
void NotifyLoginSuccess(const char* username, int score, int softcore_score, int unread_messages);
/// Notify that a challenge indicator should be shown
void NotifyChallengeIndicatorShow(const rc_client_achievement_t* achievement);
/// Notify that progress towards an achievement was made
void NotifyProgressIndicatorUpdate(const rc_client_achievement_t* achievement);
/// Notify game summary information
void NotifyGameSummary(const char* game_title, const rc_client_user_game_summary_t* summary);
/// Show a generic notification
void ShowNotification(const char* message, bool is_long = false);
} // namespace AchievementsAndroid
+282
View File
@@ -0,0 +1,282 @@
// SPDX-FileCopyrightText: 2025 Android Port Contributors
// SPDX-License-Identifier: GPL-3.0+
#include <jni.h>
#include <string>
#include <mutex>
#include <android/log.h>
#include "pcsx2/Achievements.h"
#define LOG_TAG "PCSX2Achievements"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
namespace AchievementsJNI
{
static JavaVM* s_jvm = nullptr;
static jclass s_achievements_class = nullptr;
static std::mutex s_jni_mutex;
// Method IDs for Java callbacks
static jmethodID s_on_achievement_unlocked = nullptr;
static jmethodID s_on_game_complete = nullptr;
static jmethodID s_on_leaderboard_started = nullptr;
static jmethodID s_on_leaderboard_submitted = nullptr;
static jmethodID s_on_login_success = nullptr;
static jmethodID s_on_challenge_indicator_show = nullptr;
static jmethodID s_on_progress_indicator_update = nullptr;
static jmethodID s_on_game_summary = nullptr;
static jmethodID s_show_notification = nullptr;
bool Initialize(JNIEnv* env)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
if (env->GetJavaVM(&s_jvm) != JNI_OK)
{
LOGE("Failed to get JavaVM");
return false;
}
jclass local_class = env->FindClass("com/izzy2lost/psx2/RetroAchievementsManager");
if (!local_class)
{
LOGE("Failed to find RetroAchievementsManager class");
env->ExceptionClear(); // Clear any pending exception
return false;
}
s_achievements_class = static_cast<jclass>(env->NewGlobalRef(local_class));
env->DeleteLocalRef(local_class);
if (!s_achievements_class)
{
LOGE("Failed to create global reference");
return false;
}
// Cache method IDs - clear exceptions after each attempt
s_on_achievement_unlocked = env->GetStaticMethodID(s_achievements_class, "onAchievementUnlocked",
"(Ljava/lang/String;Ljava/lang/String;IZ)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_game_complete = env->GetStaticMethodID(s_achievements_class, "onGameComplete",
"(Ljava/lang/String;II)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_leaderboard_started = env->GetStaticMethodID(s_achievements_class, "onLeaderboardStarted",
"(Ljava/lang/String;)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_leaderboard_submitted = env->GetStaticMethodID(s_achievements_class, "onLeaderboardSubmitted",
"(Ljava/lang/String;Ljava/lang/String;II)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_login_success = env->GetStaticMethodID(s_achievements_class, "onLoginSuccess",
"(Ljava/lang/String;III)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_challenge_indicator_show = env->GetStaticMethodID(s_achievements_class, "onChallengeIndicatorShow",
"(Ljava/lang/String;)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_progress_indicator_update = env->GetStaticMethodID(s_achievements_class, "onProgressIndicatorUpdate",
"(Ljava/lang/String;Ljava/lang/String;)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_on_game_summary = env->GetStaticMethodID(s_achievements_class, "onGameSummary",
"(Ljava/lang/String;IIIIZ)V");
if (env->ExceptionCheck()) env->ExceptionClear();
s_show_notification = env->GetStaticMethodID(s_achievements_class, "showNotification",
"(Ljava/lang/String;I)V");
if (env->ExceptionCheck()) env->ExceptionClear();
if (!s_on_achievement_unlocked || !s_on_game_complete || !s_on_leaderboard_started ||
!s_on_leaderboard_submitted || !s_on_login_success || !s_on_challenge_indicator_show ||
!s_on_progress_indicator_update || !s_on_game_summary || !s_show_notification)
{
LOGE("Failed to find one or more method IDs");
return false;
}
LOGI("Initialized successfully");
return true;
}
void Shutdown()
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
if (!s_jvm)
return;
JNIEnv* env = nullptr;
if (s_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) == JNI_OK && env)
{
if (s_achievements_class)
{
env->DeleteGlobalRef(s_achievements_class);
s_achievements_class = nullptr;
}
}
s_jvm = nullptr;
}
JNIEnv* GetEnv()
{
if (!s_jvm)
return nullptr;
JNIEnv* env = nullptr;
if (s_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
{
// Try to attach the current thread
if (s_jvm->AttachCurrentThread(&env, nullptr) != JNI_OK)
return nullptr;
}
return env;
}
void OnAchievementUnlocked(const char* title, const char* description, int points, bool is_hardcore)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_achievement_unlocked)
return;
jstring j_title = env->NewStringUTF(title);
jstring j_description = env->NewStringUTF(description);
env->CallStaticVoidMethod(s_achievements_class, s_on_achievement_unlocked,
j_title, j_description, points, is_hardcore);
env->DeleteLocalRef(j_title);
env->DeleteLocalRef(j_description);
}
void OnGameComplete(const char* game_title, int achievement_count, int total_points)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_game_complete)
return;
jstring j_game_title = env->NewStringUTF(game_title);
env->CallStaticVoidMethod(s_achievements_class, s_on_game_complete,
j_game_title, achievement_count, total_points);
env->DeleteLocalRef(j_game_title);
}
void OnLeaderboardStarted(const char* leaderboard_title)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_leaderboard_started)
return;
jstring j_title = env->NewStringUTF(leaderboard_title);
env->CallStaticVoidMethod(s_achievements_class, s_on_leaderboard_started, j_title);
env->DeleteLocalRef(j_title);
}
void OnLeaderboardSubmitted(const char* leaderboard_title, const char* score, int rank, int total_entries)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_leaderboard_submitted)
return;
jstring j_title = env->NewStringUTF(leaderboard_title);
jstring j_score = env->NewStringUTF(score);
env->CallStaticVoidMethod(s_achievements_class, s_on_leaderboard_submitted,
j_title, j_score, rank, total_entries);
env->DeleteLocalRef(j_title);
env->DeleteLocalRef(j_score);
}
void OnLoginSuccess(const char* username, int score, int softcore_score, int unread_messages)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_login_success)
return;
jstring j_username = env->NewStringUTF(username);
env->CallStaticVoidMethod(s_achievements_class, s_on_login_success,
j_username, score, softcore_score, unread_messages);
env->DeleteLocalRef(j_username);
}
void OnChallengeIndicatorShow(const char* achievement_title)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_challenge_indicator_show)
return;
jstring j_title = env->NewStringUTF(achievement_title);
env->CallStaticVoidMethod(s_achievements_class, s_on_challenge_indicator_show, j_title);
env->DeleteLocalRef(j_title);
}
void OnProgressIndicatorUpdate(const char* achievement_title, const char* progress)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_progress_indicator_update)
return;
jstring j_title = env->NewStringUTF(achievement_title);
jstring j_progress = env->NewStringUTF(progress);
env->CallStaticVoidMethod(s_achievements_class, s_on_progress_indicator_update,
j_title, j_progress);
env->DeleteLocalRef(j_title);
env->DeleteLocalRef(j_progress);
}
void OnGameSummary(const char* game_title, int unlocked_count, int total_count,
int earned_points, int total_points, bool is_hardcore)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_on_game_summary)
return;
jstring j_game_title = env->NewStringUTF(game_title);
env->CallStaticVoidMethod(s_achievements_class, s_on_game_summary,
j_game_title, unlocked_count, total_count, earned_points, total_points, is_hardcore);
env->DeleteLocalRef(j_game_title);
}
void ShowNotification(const char* message, int duration)
{
std::lock_guard<std::mutex> lock(s_jni_mutex);
JNIEnv* env = GetEnv();
if (!env || !s_achievements_class || !s_show_notification)
return;
jstring j_message = env->NewStringUTF(message);
env->CallStaticVoidMethod(s_achievements_class, s_show_notification, j_message, duration);
env->DeleteLocalRef(j_message);
}
} // namespace AchievementsJNI
+44
View File
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2025 Android Port Contributors
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <jni.h>
namespace AchievementsJNI
{
/// Initialize the JNI bridge for achievements. Call this from JNI_OnLoad or similar.
bool Initialize(JNIEnv* env);
/// Shutdown the JNI bridge. Call this from JNI_OnUnload or similar.
void Shutdown();
/// Called when an achievement is unlocked
void OnAchievementUnlocked(const char* title, const char* description, int points, bool is_hardcore);
/// Called when the game is completed (all achievements unlocked)
void OnGameComplete(const char* game_title, int achievement_count, int total_points);
/// Called when a leaderboard attempt starts
void OnLeaderboardStarted(const char* leaderboard_title);
/// Called when a leaderboard score is submitted
void OnLeaderboardSubmitted(const char* leaderboard_title, const char* score, int rank, int total_entries);
/// Called when login is successful
void OnLoginSuccess(const char* username, int score, int softcore_score, int unread_messages);
/// Called when a challenge indicator should be shown
void OnChallengeIndicatorShow(const char* achievement_title);
/// Called when progress towards an achievement is made
void OnProgressIndicatorUpdate(const char* achievement_title, const char* progress);
/// Called when game summary is available
void OnGameSummary(const char* game_title, int unlocked_count, int total_count,
int earned_points, int total_points, bool is_hardcore);
/// Show a generic notification
void ShowNotification(const char* message, int duration);
} // namespace AchievementsJNI
@@ -0,0 +1,244 @@
// SPDX-FileCopyrightText: 2025 Android Port Contributors
// SPDX-License-Identifier: GPL-3.0+
#include <jni.h>
#include <string>
#include <android/log.h>
#include "rc_client.h"
#include "pcsx2/Achievements.h"
#include "pcsx2/Config.h"
#include "common/Console.h"
#include "common/Error.h"
extern "C" {
JNIEXPORT jboolean JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsIsActive(JNIEnv* env, jclass clazz)
{
return Achievements::IsActive() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsIsHardcoreMode(JNIEnv* env, jclass clazz)
{
return Achievements::IsHardcoreModeActive() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsHasActiveGame(JNIEnv* env, jclass clazz)
{
return Achievements::HasActiveGame() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jstring JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsGetGameTitle(JNIEnv* env, jclass clazz)
{
auto lock = Achievements::GetLock();
const std::string& title = Achievements::GetGameTitle();
return env->NewStringUTF(title.c_str());
}
JNIEXPORT jint JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsGetGameId(JNIEnv* env, jclass clazz)
{
return static_cast<jint>(Achievements::GetGameID());
}
JNIEXPORT jstring JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsGetRichPresence(JNIEnv* env, jclass clazz)
{
auto lock = Achievements::GetLock();
const std::string& presence = Achievements::GetRichPresenceString();
return env->NewStringUTF(presence.c_str());
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsLogin(JNIEnv* env, jclass clazz,
jstring username, jstring password)
{
if (!username || !password)
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Login called with null username or password");
return;
}
const char* username_str = env->GetStringUTFChars(username, nullptr);
const char* password_str = env->GetStringUTFChars(password, nullptr);
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Attempting login for user: %s", username_str);
Error error;
bool result = Achievements::Login(username_str, password_str, &error);
env->ReleaseStringUTFChars(username, username_str);
env->ReleaseStringUTFChars(password, password_str);
if (!result)
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Login failed: %s", error.GetDescription().c_str());
Console.Error("Achievements login failed: %s", error.GetDescription().c_str());
}
else
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Login successful!");
}
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsLogout(JNIEnv* env, jclass clazz)
{
Achievements::Logout();
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsInitialize(JNIEnv* env, jclass clazz)
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Initializing achievements system");
// Check if already active
if (Achievements::IsActive())
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Achievements already active, skipping initialization");
return;
}
// Enable achievements in config
EmuConfig.Achievements.Enabled = true;
// Initialize the achievements system
bool result = Achievements::Initialize();
if (result)
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Achievements initialized successfully");
}
else
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Failed to initialize achievements");
}
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsShutdown(JNIEnv* env, jclass clazz)
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Shutting down achievements system");
// Disable achievements in config
EmuConfig.Achievements.Enabled = false;
// Shutdown the achievements system
Achievements::Shutdown(false);
}
JNIEXPORT jobjectArray JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsGetAchievementList(JNIEnv* env, jclass clazz)
{
if (!Achievements::HasActiveGame())
{
__android_log_print(ANDROID_LOG_WARN, "PCSX2Achievements", "No active game, returning empty list");
return env->NewObjectArray(0, env->FindClass("com/izzy2lost/psx2/Achievement"), nullptr);
}
auto lock = Achievements::GetLock();
// Get the achievement list from rcheevos
rc_client_achievement_list_t* list = static_cast<rc_client_achievement_list_t*>(
Achievements::GetAchievementListForAndroid());
if (!list || list->num_buckets == 0)
{
__android_log_print(ANDROID_LOG_WARN, "PCSX2Achievements", "No achievements found");
if (list) rc_client_destroy_achievement_list(list);
return env->NewObjectArray(0, env->FindClass("com/izzy2lost/psx2/Achievement"), nullptr);
}
// Count total achievements
uint32_t total_count = 0;
for (uint32_t i = 0; i < list->num_buckets; i++)
{
total_count += list->buckets[i].num_achievements;
}
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Found %d achievements", total_count);
// Find Achievement class and constructor
jclass achievementClass = env->FindClass("com/izzy2lost/psx2/Achievement");
if (!achievementClass)
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Could not find Achievement class");
rc_client_destroy_achievement_list(list);
return env->NewObjectArray(0, env->FindClass("com/izzy2lost/psx2/Achievement"), nullptr);
}
jmethodID constructor = env->GetMethodID(achievementClass, "<init>",
"(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;IZJLjava/lang/String;FIFF)V");
if (!constructor)
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Could not find Achievement constructor");
rc_client_destroy_achievement_list(list);
return env->NewObjectArray(0, achievementClass, nullptr);
}
// Create array
jobjectArray result = env->NewObjectArray(total_count, achievementClass, nullptr);
if (!result)
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2Achievements", "Could not create array");
rc_client_destroy_achievement_list(list);
return env->NewObjectArray(0, achievementClass, nullptr);
}
// Fill array
uint32_t index = 0;
for (uint32_t i = 0; i < list->num_buckets; i++)
{
const rc_client_achievement_bucket_t* bucket = &list->buckets[i];
for (uint32_t j = 0; j < bucket->num_achievements; j++)
{
const rc_client_achievement_t* cheevo = bucket->achievements[j];
jstring title = env->NewStringUTF(cheevo->title ? cheevo->title : "");
jstring description = env->NewStringUTF(cheevo->description ? cheevo->description : "");
jstring badgeName = env->NewStringUTF(cheevo->badge_name);
jstring measuredProgress = env->NewStringUTF(cheevo->measured_progress);
jobject achievement = env->NewObject(achievementClass, constructor,
(jint)cheevo->id,
title,
description,
badgeName,
(jint)cheevo->points,
(jboolean)cheevo->unlocked,
(jlong)cheevo->unlock_time,
measuredProgress,
(jfloat)cheevo->measured_percent,
(jint)cheevo->state,
(jfloat)cheevo->rarity,
(jfloat)cheevo->rarity_hardcore);
env->SetObjectArrayElement(result, index++, achievement);
env->DeleteLocalRef(title);
env->DeleteLocalRef(description);
env->DeleteLocalRef(badgeName);
env->DeleteLocalRef(measuredProgress);
env->DeleteLocalRef(achievement);
}
}
rc_client_destroy_achievement_list(list);
return result;
}
JNIEXPORT void JNICALL
Java_com_izzy2lost_psx2_NativeApp_achievementsSetHardcoreMode(JNIEnv* env, jclass clazz, jboolean enabled)
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Setting hardcore mode: %d", enabled);
// Set in config - will apply on next game load
EmuConfig.Achievements.HardcoreMode = enabled;
__android_log_print(ANDROID_LOG_INFO, "PCSX2Achievements", "Hardcore mode will apply on next game load");
}
} // extern "C"
@@ -173,6 +173,13 @@ bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request)
curl_easy_setopt(req->handle, CURLOPT_PRIVATE, req);
curl_easy_setopt(req->handle, CURLOPT_FOLLOWLOCATION, 1L);
#ifdef __ANDROID__
// Android doesn't have easy access to system CA certificates
// Disable SSL verification for now - in production you'd want to bundle CA certs
curl_easy_setopt(req->handle, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(req->handle, CURLOPT_SSL_VERIFYHOST, 0L);
#endif
if (request->type == Request::Type::Post)
{
curl_easy_setopt(req->handle, CURLOPT_POST, 1L);
+22
View File
@@ -1,7 +1,9 @@
#include <jni.h>
#include <android/native_window_jni.h>
#include <android/log.h>
#include <unistd.h>
#include "PrecompiledHeader.h"
#include "AchievementsJNI.h"
#include "common/StringUtil.h"
#include "common/FileSystem.h"
#include "common/Error.h"
@@ -238,9 +240,29 @@ Java_com_izzy2lost_psx2_NativeApp_initialize(JNIEnv *env, jclass clazz,
// si.SetBoolValue("MemoryCards", fmt::format("Slot{}_Enable", i + 1).c_str(), false);
// si.SetStringValue("MemoryCards", fmt::format("Slot{}_Filename", i + 1).c_str(), "");
// }
// Enable RetroAchievements
si.SetBoolValue("Achievements", "Enabled", true);
si.SetBoolValue("Achievements", "HardcoreMode", false);
si.SetBoolValue("Achievements", "Notifications", true);
si.SetBoolValue("Achievements", "LeaderboardNotifications", true);
si.SetBoolValue("Achievements", "SoundEffects", false); // No sound on Android
si.SetBoolValue("Achievements", "EncoreMode", false);
si.SetBoolValue("Achievements", "SpectatorMode", false);
si.SetBoolValue("Achievements", "UnofficialTestMode", false);
}
VMManager::Internal::LoadStartupSettings();
// Initialize RetroAchievements JNI bridge
if (!AchievementsJNI::Initialize(env))
{
__android_log_print(ANDROID_LOG_ERROR, "PCSX2", "Failed to initialize AchievementsJNI");
}
else
{
__android_log_print(ANDROID_LOG_INFO, "PCSX2", "AchievementsJNI initialized successfully");
}
}
extern "C"
+51
View File
@@ -50,6 +50,10 @@
#include "RA_Interface.h"
#endif
#ifdef __ANDROID__
#include "../AchievementsAndroid.h"
#endif
namespace Achievements
{
static constexpr u32 LEADERBOARD_NEARBY_ENTRIES_TO_FETCH = 10;
@@ -363,6 +367,16 @@ bool Achievements::HasActiveGame()
return s_game_id != 0;
}
void* Achievements::GetAchievementListForAndroid()
{
if (!s_client || !HasActiveGame())
return nullptr;
return rc_client_create_achievement_list(s_client,
RC_CLIENT_ACHIEVEMENT_CATEGORY_CORE_AND_UNOFFICIAL,
RC_CLIENT_ACHIEVEMENT_LIST_GROUPING_PROGRESS);
}
u32 Achievements::GetGameID()
{
return s_game_id;
@@ -1010,6 +1024,9 @@ void Achievements::ClearGameHash()
void Achievements::DisplayAchievementSummary()
{
#ifdef __ANDROID__
AchievementsAndroid::NotifyGameSummary(s_game_title.c_str(), &s_game_summary);
#else
if (EmuConfig.Achievements.Notifications)
{
std::string title;
@@ -1043,6 +1060,7 @@ void Achievements::DisplayAchievementSummary()
}
});
}
#endif
#if !defined(__ANDROID__)
if (EmuConfig.Achievements.SoundEffects && EmuConfig.Achievements.InfoSound)
@@ -1081,6 +1099,10 @@ void Achievements::HandleUnlockEvent(const rc_client_event_t* event)
Console.WriteLn("Achievements: Achievement %s (%u) for game %u unlocked", cheevo->title, cheevo->id, s_game_id);
UpdateGameSummary();
#ifdef __ANDROID__
AchievementsAndroid::NotifyAchievementUnlocked(cheevo);
#endif
if (EmuConfig.Achievements.Notifications)
{
std::string title;
@@ -1109,6 +1131,11 @@ void Achievements::HandleGameCompleteEvent(const rc_client_event_t* event)
Console.WriteLn("Achievements: Game %u complete", s_game_id);
UpdateGameSummary();
#ifdef __ANDROID__
AchievementsAndroid::NotifyGameComplete(s_game_title.c_str(),
s_game_summary.num_unlocked_achievements, s_game_summary.points_unlocked);
#endif
if (EmuConfig.Achievements.Notifications)
{
std::string title = fmt::format(TRANSLATE_FS("Achievements", "Mastered {}"), s_game_title);
@@ -1132,6 +1159,10 @@ void Achievements::HandleLeaderboardStartedEvent(const rc_client_event_t* event)
{
DevCon.WriteLn("Achievements: Leaderboard %u (%s) started", event->leaderboard->id, event->leaderboard->title);
#ifdef __ANDROID__
AchievementsAndroid::NotifyLeaderboardStarted(event->leaderboard);
#endif
if (EmuConfig.Achievements.LeaderboardNotifications)
{
std::string title = event->leaderboard->title;
@@ -1205,6 +1236,13 @@ void Achievements::HandleLeaderboardScoreboardEvent(const rc_client_event_t* eve
Console.WriteLn("Achievements: Leaderboard %u scoreboard rank %u of %u", event->leaderboard_scoreboard->leaderboard_id,
event->leaderboard_scoreboard->new_rank, event->leaderboard_scoreboard->num_entries);
#ifdef __ANDROID__
AchievementsAndroid::NotifyLeaderboardSubmitted(event->leaderboard,
event->leaderboard_scoreboard->submitted_score,
event->leaderboard_scoreboard->new_rank,
event->leaderboard_scoreboard->num_entries);
#endif
if (EmuConfig.Achievements.LeaderboardNotifications)
{
static const char* value_strings[NUM_RC_CLIENT_LEADERBOARD_FORMATS] = {
@@ -1287,6 +1325,10 @@ void Achievements::HandleAchievementChallengeIndicatorShowEvent(const rc_client_
s_active_challenge_indicators.push_back(std::move(indicator));
DevCon.WriteLn("Achievements: Show challenge indicator for %u (%s)", event->achievement->id, event->achievement->title);
#ifdef __ANDROID__
AchievementsAndroid::NotifyChallengeIndicatorShow(event->achievement);
#endif
}
void Achievements::HandleAchievementChallengeIndicatorHideEvent(const rc_client_event_t* event)
@@ -1314,6 +1356,10 @@ void Achievements::HandleAchievementProgressIndicatorShowEvent(const rc_client_e
s_active_progress_indicator->achievement = event->achievement;
s_active_progress_indicator->badge_path = GetAchievementBadgePath(event->achievement, RC_CLIENT_ACHIEVEMENT_STATE_UNLOCKED);
s_active_progress_indicator->active = true;
#ifdef __ANDROID__
AchievementsAndroid::NotifyProgressIndicatorUpdate(event->achievement);
#endif
}
void Achievements::HandleAchievementProgressIndicatorHideEvent(const rc_client_event_t* event)
@@ -1745,6 +1791,11 @@ void Achievements::ShowLoginSuccess(const rc_client_t* client)
Host::OnAchievementsLoginSuccess(user->username, user->score, user->score_softcore, user->num_unread_messages);
#ifdef __ANDROID__
AchievementsAndroid::NotifyLoginSuccess(user->username, user->score,
user->score_softcore, user->num_unread_messages);
#endif
// Were we logging in with a temporary client?
const auto lock = GetLock();
if (s_client != client)
+4
View File
@@ -89,6 +89,10 @@ namespace Achievements
/// Returns true if RetroAchievements game data has been loaded.
bool HasActiveGame();
/// Gets the achievement list for Android (returns nullptr if no game loaded).
/// Caller must call rc_client_destroy_achievement_list() when done.
void* GetAchievementListForAndroid();
/// Returns the RetroAchievements ID for the current game.
u32 GetGameID();
+3
View File
@@ -1124,6 +1124,9 @@ if(ANDROID)
${pcsx2LinuxSources}
${pcsx2LinuxHeaders}
../native-lib.cpp
../AchievementsJNI.cpp
../AchievementsAndroid.cpp
../AchievementsNativeMethods.cpp
)
elseif(LINUX)
target_sources(PCSX2 PRIVATE
@@ -0,0 +1,55 @@
package com.izzy2lost.psx2;
/**
* Represents a single achievement from RetroAchievements.
*/
public class Achievement {
public final int id;
public final String title;
public final String description;
public final String badgeName;
public final int points;
public final boolean unlocked;
public final long unlockTime;
public final String measuredProgress;
public final float measuredPercent;
public final int state;
public final float rarity;
public final float rarityHardcore;
public Achievement(int id, String title, String description, String badgeName,
int points, boolean unlocked, long unlockTime,
String measuredProgress, float measuredPercent, int state,
float rarity, float rarityHardcore) {
this.id = id;
this.title = title;
this.description = description;
this.badgeName = badgeName;
this.points = points;
this.unlocked = unlocked;
this.unlockTime = unlockTime;
this.measuredProgress = measuredProgress;
this.measuredPercent = measuredPercent;
this.state = state;
this.rarity = rarity;
this.rarityHardcore = rarityHardcore;
}
/**
* Get the badge URL for this achievement.
* @param locked Whether to get the locked or unlocked badge
*/
public String getBadgeUrl(boolean locked) {
return String.format("https://media.retroachievements.org/Badge/%s%s.png",
badgeName, locked ? "_lock" : "");
}
/**
* Get a formatted string for the unlock time.
*/
public String getUnlockTimeFormatted() {
if (unlockTime == 0) return "Not unlocked";
return new java.text.SimpleDateFormat("MMM dd, yyyy", java.util.Locale.US)
.format(new java.util.Date(unlockTime * 1000));
}
}
@@ -0,0 +1,397 @@
package com.izzy2lost.psx2;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
/**
* Dialog for managing RetroAchievements settings and login.
*/
public class AchievementsDialogFragment extends DialogFragment {
private static final String PREFS_NAME = "RetroAchievements";
private static final String PREF_ENABLED = "enabled";
private static final String PREF_HARDCORE = "hardcore_mode";
private static final String PREF_NOTIFICATIONS = "notifications";
private static final String PREF_USERNAME = "username";
private static final String PREF_REMEMBER_ME = "remember_me";
private static final String PREF_SAVED_PASSWORD = "saved_password";
private CheckBox mEnabledCheckbox;
private CheckBox mHardcoreModeCheckbox;
private CheckBox mNotificationsCheckbox;
private CheckBox mRememberMeCheckbox;
private EditText mUsernameEdit;
private EditText mPasswordEdit;
private TextView mStatusText;
private com.google.android.material.button.MaterialButton mLoginButton;
private com.google.android.material.button.MaterialButton mLogoutButton;
private com.google.android.material.button.MaterialButton mCreateAccountButton;
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Context context = requireContext();
LayoutInflater inflater = LayoutInflater.from(context);
// For now, create a simple layout programmatically
// In production, you'd want to create a proper XML layout
View view = createView(context);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle("RetroAchievements")
.setView(view)
.setPositiveButton("Close", (dialog, which) -> {
saveSettings();
dismiss();
});
return builder.create();
}
private View createView(Context context) {
// Create a ScrollView to contain everything
android.widget.ScrollView scrollView = new android.widget.ScrollView(context);
// Create a simple vertical layout with settings
android.widget.LinearLayout layout = new android.widget.LinearLayout(context);
layout.setOrientation(android.widget.LinearLayout.VERTICAL);
layout.setPadding(48, 24, 48, 24);
// Status text
mStatusText = new TextView(context);
mStatusText.setPadding(0, 0, 0, 24);
updateStatus();
layout.addView(mStatusText);
// Enabled checkbox
mEnabledCheckbox = new CheckBox(context);
mEnabledCheckbox.setText("Enable RetroAchievements");
mEnabledCheckbox.setChecked(getPrefs().getBoolean(PREF_ENABLED, false));
mEnabledCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
android.util.Log.d("Achievements", "Enable checkbox changed to: " + isChecked);
getPrefs().edit().putBoolean(PREF_ENABLED, isChecked).apply();
if (isChecked) {
// Initialize achievements system
new Thread(() -> {
android.util.Log.d("Achievements", "Initializing achievements system...");
NativeApp.achievementsInitialize();
}).start();
} else {
// Shutdown achievements system
new Thread(() -> {
android.util.Log.d("Achievements", "Shutting down achievements system...");
NativeApp.achievementsShutdown();
}).start();
}
updateUIState();
});
layout.addView(mEnabledCheckbox);
// Hardcore mode checkbox
mHardcoreModeCheckbox = new CheckBox(context);
mHardcoreModeCheckbox.setText("Hardcore Mode");
mHardcoreModeCheckbox.setChecked(getPrefs().getBoolean(PREF_HARDCORE, false));
mHardcoreModeCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> {
android.util.Log.d("Achievements", "Hardcore mode changed to: " + isChecked);
NativeApp.achievementsSetHardcoreMode(isChecked);
});
layout.addView(mHardcoreModeCheckbox);
// Notifications checkbox
mNotificationsCheckbox = new CheckBox(context);
mNotificationsCheckbox.setText("Show Notifications");
mNotificationsCheckbox.setChecked(getPrefs().getBoolean(PREF_NOTIFICATIONS, true));
layout.addView(mNotificationsCheckbox);
// Spacer
View spacer1 = new View(context);
spacer1.setMinimumHeight(16);
layout.addView(spacer1);
// Username field
mUsernameEdit = new EditText(context);
mUsernameEdit.setHint("Username");
String savedUsername = getPrefs().getString(PREF_USERNAME, "");
mUsernameEdit.setText(savedUsername);
layout.addView(mUsernameEdit);
// Password field
mPasswordEdit = new EditText(context);
mPasswordEdit.setHint("Password");
mPasswordEdit.setInputType(android.text.InputType.TYPE_CLASS_TEXT |
android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD);
// Load saved password if remember me is enabled
boolean rememberMe = getPrefs().getBoolean(PREF_REMEMBER_ME, false);
if (rememberMe) {
String savedPassword = getPrefs().getString(PREF_SAVED_PASSWORD, "");
mPasswordEdit.setText(savedPassword);
}
layout.addView(mPasswordEdit);
// Remember Me checkbox
mRememberMeCheckbox = new CheckBox(context);
mRememberMeCheckbox.setText("Remember Me");
mRememberMeCheckbox.setChecked(rememberMe);
layout.addView(mRememberMeCheckbox);
// Login button
mLoginButton = new com.google.android.material.button.MaterialButton(context);
mLoginButton.setText("Login");
mLoginButton.setOnClickListener(v -> performLogin());
layout.addView(mLoginButton);
// Logout button
mLogoutButton = new com.google.android.material.button.MaterialButton(context);
mLogoutButton.setText("Logout");
mLogoutButton.setOnClickListener(v -> performLogout());
layout.addView(mLogoutButton);
// Spacer
View spacer2 = new View(context);
spacer2.setMinimumHeight(16);
layout.addView(spacer2);
// View Achievements List button
com.google.android.material.button.MaterialButton viewListButton = new com.google.android.material.button.MaterialButton(context);
viewListButton.setText("View Achievements List");
viewListButton.setOnClickListener(v -> openAchievementsList());
layout.addView(viewListButton);
// View Profile button
com.google.android.material.button.MaterialButton viewProfileButton = new com.google.android.material.button.MaterialButton(context);
viewProfileButton.setText("View Profile on RetroAchievements.org");
viewProfileButton.setOnClickListener(v -> openProfilePage());
layout.addView(viewProfileButton);
// Spacer
View spacer3 = new View(context);
spacer3.setMinimumHeight(8);
layout.addView(spacer3);
// Create Account link
TextView createAccountText = new TextView(context);
createAccountText.setText("Don't have an account?");
createAccountText.setTextSize(12);
createAccountText.setPadding(0, 8, 0, 4);
layout.addView(createAccountText);
mCreateAccountButton = new com.google.android.material.button.MaterialButton(context);
mCreateAccountButton.setText("Create Free Account");
mCreateAccountButton.setOnClickListener(v -> openCreateAccountPage());
layout.addView(mCreateAccountButton);
// Add layout to ScrollView
scrollView.addView(layout);
updateUIState();
return scrollView;
}
private void updateStatus() {
if (mStatusText == null) return;
if (NativeApp.achievementsIsActive()) {
if (NativeApp.achievementsHasActiveGame()) {
String gameTitle = NativeApp.achievementsGetGameTitle();
String richPresence = NativeApp.achievementsGetRichPresence();
boolean isHardcore = NativeApp.achievementsIsHardcoreMode();
mStatusText.setText(String.format("Active: %s\n%s\nMode: %s",
gameTitle, richPresence, isHardcore ? "Hardcore" : "Softcore"));
} else {
mStatusText.setText("Active: No game loaded\n\nNote: This is an unofficial build. Hardcore achievements may not be available.");
}
} else {
mStatusText.setText("Status: Inactive\n\nNote: This is an unofficial build. You can still earn softcore achievements!");
}
}
private void updateUIState() {
if (mEnabledCheckbox == null || mLoginButton == null || mLogoutButton == null) {
return;
}
boolean enabled = mEnabledCheckbox.isChecked();
// Check if we're logged in by checking if we have a saved username
String savedUsername = getPrefs().getString(PREF_USERNAME, "");
boolean hasCredentials = !savedUsername.isEmpty();
if (mHardcoreModeCheckbox != null) mHardcoreModeCheckbox.setEnabled(enabled);
if (mNotificationsCheckbox != null) mNotificationsCheckbox.setEnabled(enabled);
if (mRememberMeCheckbox != null) mRememberMeCheckbox.setEnabled(enabled);
if (mUsernameEdit != null) mUsernameEdit.setEnabled(enabled);
if (mPasswordEdit != null) mPasswordEdit.setEnabled(enabled);
// Login button: enabled when achievements are enabled
mLoginButton.setEnabled(enabled);
mLoginButton.setVisibility(View.VISIBLE);
// Logout button: enabled if we have saved credentials
mLogoutButton.setEnabled(enabled && hasCredentials);
mLogoutButton.setVisibility(View.VISIBLE);
// Create account button: always enabled
if (mCreateAccountButton != null) {
mCreateAccountButton.setEnabled(true);
mCreateAccountButton.setVisibility(View.VISIBLE);
}
android.util.Log.d("Achievements", "updateUIState - enabled: " + enabled +
", hasCredentials: " + hasCredentials +
", loginEnabled: " + mLoginButton.isEnabled() +
", logoutEnabled: " + mLogoutButton.isEnabled());
}
private void performLogin() {
String username = mUsernameEdit.getText().toString().trim();
String password = mPasswordEdit.getText().toString().trim();
boolean rememberMe = mRememberMeCheckbox.isChecked();
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
Toast.makeText(requireContext(), "Please enter username and password",
Toast.LENGTH_SHORT).show();
return;
}
android.util.Log.d("Achievements", "Starting login for user: " + username);
Toast.makeText(requireContext(), "Logging in...", Toast.LENGTH_SHORT).show();
// Save username and remember me preference
SharedPreferences.Editor editor = getPrefs().edit();
editor.putString(PREF_USERNAME, username);
editor.putBoolean(PREF_REMEMBER_ME, rememberMe);
// Save password only if remember me is checked
if (rememberMe) {
editor.putString(PREF_SAVED_PASSWORD, password);
} else {
editor.remove(PREF_SAVED_PASSWORD);
}
editor.apply();
// Perform login on background thread
new Thread(() -> {
android.util.Log.d("Achievements", "Calling native login...");
NativeApp.achievementsLogin(username, password);
android.util.Log.d("Achievements", "Native login call completed");
// Wait a bit for the login to process
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Update UI on main thread
requireActivity().runOnUiThread(() -> {
boolean isActive = NativeApp.achievementsIsActive();
android.util.Log.d("Achievements", "After login - isActive: " + isActive);
if (isActive) {
Toast.makeText(requireContext(), "Login successful!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(requireContext(), "Login may have failed - check logs", Toast.LENGTH_LONG).show();
}
updateStatus();
updateUIState();
});
}).start();
}
private void performLogout() {
NativeApp.achievementsLogout();
// Clear saved password on logout
getPrefs().edit()
.remove(PREF_SAVED_PASSWORD)
.putBoolean(PREF_REMEMBER_ME, false)
.apply();
// Clear password field
if (mPasswordEdit != null) {
mPasswordEdit.setText("");
}
if (mRememberMeCheckbox != null) {
mRememberMeCheckbox.setChecked(false);
}
updateStatus();
updateUIState();
Toast.makeText(requireContext(), "Logged out", Toast.LENGTH_SHORT).show();
}
private void openAchievementsList() {
if (!NativeApp.achievementsHasActiveGame()) {
Toast.makeText(requireContext(), "No game loaded", Toast.LENGTH_SHORT).show();
return;
}
AchievementsListDialogFragment dialog = AchievementsListDialogFragment.newInstance();
dialog.show(getParentFragmentManager(), "achievements_list");
}
private void openProfilePage() {
try {
String username = getPrefs().getString(PREF_USERNAME, "");
if (username.isEmpty()) {
Toast.makeText(requireContext(), "Please login first", Toast.LENGTH_SHORT).show();
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://retroachievements.org/user/" + username));
startActivity(intent);
} catch (Exception e) {
Toast.makeText(requireContext(), "Could not open browser", Toast.LENGTH_SHORT).show();
}
}
private void openCreateAccountPage() {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://retroachievements.org/createaccount.php"));
startActivity(intent);
} catch (Exception e) {
Toast.makeText(requireContext(), "Could not open browser", Toast.LENGTH_SHORT).show();
}
}
private void saveSettings() {
SharedPreferences.Editor editor = getPrefs().edit();
editor.putBoolean(PREF_ENABLED, mEnabledCheckbox.isChecked());
editor.putBoolean(PREF_HARDCORE, mHardcoreModeCheckbox.isChecked());
editor.putBoolean(PREF_NOTIFICATIONS, mNotificationsCheckbox.isChecked());
editor.apply();
android.util.Log.d("Achievements", "Settings saved");
}
private SharedPreferences getPrefs() {
return requireContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public static AchievementsDialogFragment newInstance() {
return new AchievementsDialogFragment();
}
}
@@ -0,0 +1,145 @@
package com.izzy2lost.psx2;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
/**
* Dialog showing the list of achievements for the current game.
*/
public class AchievementsListDialogFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
builder.setTitle("Achievements");
View view = createView();
builder.setView(view);
builder.setPositiveButton("Close", null);
return builder.create();
}
private View createView() {
ScrollView scrollView = new ScrollView(requireContext());
LinearLayout layout = new LinearLayout(requireContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(24, 24, 24, 24);
// Load achievements on background thread
new Thread(() -> {
try {
Achievement[] achievements = NativeApp.achievementsGetAchievementList();
requireActivity().runOnUiThread(() -> {
if (achievements == null || achievements.length == 0) {
TextView emptyText = new TextView(requireContext());
emptyText.setText("No achievements found for this game.");
emptyText.setPadding(0, 16, 0, 16);
layout.addView(emptyText);
} else {
// Count unlocked
int unlockedCount = 0;
int totalPoints = 0;
int earnedPoints = 0;
for (Achievement achievement : achievements) {
if (achievement.unlocked) {
unlockedCount++;
earnedPoints += achievement.points;
}
totalPoints += achievement.points;
}
// Summary
TextView summary = new TextView(requireContext());
summary.setText(String.format("Unlocked: %d / %d (%d / %d points)",
unlockedCount, achievements.length, earnedPoints, totalPoints));
summary.setTextSize(16);
summary.setPadding(0, 0, 0, 16);
layout.addView(summary);
// Add each achievement
for (Achievement achievement : achievements) {
layout.addView(createAchievementView(achievement));
}
}
});
} catch (Exception e) {
requireActivity().runOnUiThread(() -> {
Toast.makeText(requireContext(), "Error loading achievements: " + e.getMessage(),
Toast.LENGTH_SHORT).show();
});
}
}).start();
scrollView.addView(layout);
return scrollView;
}
private View createAchievementView(Achievement achievement) {
LinearLayout itemLayout = new LinearLayout(requireContext());
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
itemLayout.setPadding(0, 8, 0, 8);
// Icon placeholder (we'll add image loading later)
TextView icon = new TextView(requireContext());
icon.setText(achievement.unlocked ? "🏆" : "🔒");
icon.setTextSize(32);
icon.setPadding(0, 0, 16, 0);
itemLayout.addView(icon);
// Text content
LinearLayout textLayout = new LinearLayout(requireContext());
textLayout.setOrientation(LinearLayout.VERTICAL);
textLayout.setLayoutParams(new LinearLayout.LayoutParams(
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f));
TextView title = new TextView(requireContext());
title.setText(achievement.title + " (" + achievement.points + " pts)");
title.setTextSize(14);
title.setTypeface(null, android.graphics.Typeface.BOLD);
textLayout.addView(title);
TextView description = new TextView(requireContext());
description.setText(achievement.description);
description.setTextSize(12);
description.setAlpha(0.7f);
textLayout.addView(description);
if (achievement.unlocked) {
TextView unlockTime = new TextView(requireContext());
unlockTime.setText("Unlocked: " + achievement.getUnlockTimeFormatted());
unlockTime.setTextSize(10);
unlockTime.setAlpha(0.5f);
textLayout.addView(unlockTime);
} else if (achievement.measuredPercent > 0) {
TextView progress = new TextView(requireContext());
progress.setText(String.format("Progress: %.1f%%", achievement.measuredPercent));
progress.setTextSize(10);
progress.setAlpha(0.5f);
textLayout.addView(progress);
}
itemLayout.addView(textLayout);
return itemLayout;
}
public static AchievementsListDialogFragment newInstance() {
return new AchievementsListDialogFragment();
}
}
@@ -551,6 +551,16 @@ public class GamesCoverDialogFragment extends DialogFragment {
} catch (Throwable ignored) {}
});
}
View btnAchievements = header.findViewById(R.id.drawer_btn_achievements);
if (btnAchievements != null) {
btnAchievements.setOnClickListener(v -> {
try {
AchievementsDialogFragment achievementsDialog = AchievementsDialogFragment.newInstance();
achievementsDialog.show(getParentFragmentManager(), "achievements");
} catch (Throwable ignored) {}
});
}
// Setup drawer settings controls to mirror quick actions
setupDialogDrawerSettings(header);
@@ -454,6 +454,9 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
Initialize();
// Initialize RetroAchievements
RetroAchievementsManager.initialize(this);
// Initialize controller input handler
mControllerInputHandler = new ControllerInputHandler(this);
@@ -689,6 +692,20 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
} catch (Throwable ignored) {}
});
}
// Achievements button
View btnAchievements = header.findViewById(R.id.drawer_btn_achievements);
if (btnAchievements != null) {
btnAchievements.setOnClickListener(v -> {
try {
android.util.Log.d("MainActivity", "Achievements button clicked");
AchievementsDialogFragment dialog = AchievementsDialogFragment.newInstance();
dialog.show(getSupportFragmentManager(), "achievements_dialog");
} catch (Throwable e) {
android.util.Log.e("MainActivity", "Error showing achievements dialog: " + e.getMessage());
}
});
}
// Setup drawer settings controls to mirror quick actions
setupDrawerSettings(header);
@@ -1590,6 +1607,36 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
btn_pause_play.setIcon(ContextCompat.getDrawable(this, R.drawable.pause_circle_24px));
}
});
// Auto-initialize and login to achievements if enabled
SharedPreferences prefs = getSharedPreferences("RetroAchievements", MODE_PRIVATE);
boolean achievementsEnabled = prefs.getBoolean("enabled", false);
if (achievementsEnabled) {
String username = prefs.getString("username", "");
boolean rememberMe = prefs.getBoolean("remember_me", false);
String savedPassword = prefs.getString("saved_password", "");
new Thread(() -> {
try {
Thread.sleep(3000); // Wait 3 seconds for game to start
// Initialize if not already active
if (!NativeApp.achievementsIsActive()) {
android.util.Log.d("Achievements", "Auto-initializing achievements for game");
NativeApp.achievementsInitialize();
Thread.sleep(500); // Wait for initialization
}
// Auto-login if credentials are saved
if (!username.isEmpty() && rememberMe && !savedPassword.isEmpty()) {
android.util.Log.d("Achievements", "Auto-logging in as: " + username);
NativeApp.achievementsLogin(username, savedPassword);
}
} catch (Exception e) {
android.util.Log.e("Achievements", "Failed to auto-initialize/login: " + e.getMessage());
}
}).start();
}
}
}
@@ -2771,4 +2818,5 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
}
}
}
@@ -155,6 +155,20 @@ public class NativeApp {
// Returns array of strings in format "filename|size|isDirectory"
public static native String[] getMemoryCardSaves(String memcardPath);
// RetroAchievements native methods
public static native boolean achievementsIsActive();
public static native boolean achievementsIsHardcoreMode();
public static native boolean achievementsHasActiveGame();
public static native String achievementsGetGameTitle();
public static native int achievementsGetGameId();
public static native String achievementsGetRichPresence();
public static native void achievementsLogin(String username, String password);
public static native void achievementsLogout();
public static native void achievementsInitialize();
public static native void achievementsShutdown();
public static native Achievement[] achievementsGetAchievementList();
public static native void achievementsSetHardcoreMode(boolean enabled);
public static native void onNativeSurfaceCreated();
public static native void onNativeSurfaceChanged(Surface surface, int w, int h);
public static native void onNativeSurfaceDestroyed();
@@ -249,6 +249,15 @@ public class QuickActionsDialogFragment extends DialogFragment {
});
}
// Achievements: open achievements dialog
MaterialButton btnAchievements = view.findViewById(R.id.btn_quick_achievements);
if (btnAchievements != null) {
btnAchievements.setOnClickListener(v -> {
try { AchievementsDialogFragment.newInstance().show(getParentFragmentManager(), "achievements_dialog"); } catch (Throwable ignored) {}
dismissAllowingStateLoss();
});
}
// Exit Game: open games dialog
if (btnExitGame != null) {
btnExitGame.setOnClickListener(v -> {
@@ -0,0 +1,219 @@
package com.izzy2lost.psx2;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import java.lang.ref.WeakReference;
/**
* Manages RetroAchievements integration for the Android app.
* Handles achievement unlock notifications, leaderboard updates, and rich presence.
*/
public class RetroAchievementsManager {
private static WeakReference<Activity> sActivity;
private static final Handler sMainHandler = new Handler(Looper.getMainLooper());
// Achievement notification types
public static final int NOTIFICATION_ACHIEVEMENT_UNLOCKED = 0;
public static final int NOTIFICATION_GAME_COMPLETE = 1;
public static final int NOTIFICATION_LEADERBOARD_STARTED = 2;
public static final int NOTIFICATION_LEADERBOARD_SUBMITTED = 3;
public static final int NOTIFICATION_LOGIN_SUCCESS = 4;
public static final int NOTIFICATION_CHALLENGE_INDICATOR = 5;
public static final int NOTIFICATION_PROGRESS_INDICATOR = 6;
/**
* Initialize the RetroAchievements manager with the main activity.
*/
public static void initialize(Activity activity) {
sActivity = new WeakReference<>(activity);
}
/**
* Called from native code when an achievement is unlocked.
* @param title Achievement title
* @param description Achievement description
* @param points Points earned
* @param isHardcore Whether hardcore mode is active
*/
public static void onAchievementUnlocked(final String title, final String description,
final int points, final boolean isHardcore) {
android.util.Log.i("Achievements", "Achievement unlocked: " + title + " (" + points + " points)");
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) {
android.util.Log.w("Achievements", "Activity is null, cannot show toast");
return;
}
// Check if notifications are enabled
android.content.SharedPreferences prefs = activity.getSharedPreferences("RetroAchievements", android.content.Context.MODE_PRIVATE);
boolean notificationsEnabled = prefs.getBoolean("notifications", true);
if (!notificationsEnabled) {
android.util.Log.d("Achievements", "Notifications disabled, skipping toast");
return;
}
String message = String.format("🏆 Achievement Unlocked!\n%s\n%s\n%d points%s",
title, description, points,
isHardcore ? " (Hardcore)" : "");
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
});
}
/**
* Called from native code when the game is completed (all achievements unlocked).
* @param gameTitle Game title
* @param achievementCount Total achievements unlocked
* @param totalPoints Total points earned
*/
public static void onGameComplete(final String gameTitle, final int achievementCount,
final int totalPoints) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("🏆 Mastered %s!\n%d achievements, %d points",
gameTitle, achievementCount, totalPoints);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
});
}
/**
* Called from native code when a leaderboard attempt starts.
* @param leaderboardTitle Leaderboard title
*/
public static void onLeaderboardStarted(final String leaderboardTitle) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("📊 %s\nLeaderboard attempt started", leaderboardTitle);
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
});
}
/**
* Called from native code when a leaderboard score is submitted.
* @param leaderboardTitle Leaderboard title
* @param score Score value
* @param rank Player's rank
* @param totalEntries Total entries in leaderboard
*/
public static void onLeaderboardSubmitted(final String leaderboardTitle, final String score,
final int rank, final int totalEntries) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("📊 %s\nScore: %s\nRank: %d of %d",
leaderboardTitle, score, rank, totalEntries);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
});
}
/**
* Called from native code when successfully logged in.
* @param username User's display name
* @param score Total score
* @param softcoreScore Softcore score
* @param unreadMessages Number of unread messages
*/
public static void onLoginSuccess(final String username, final int score,
final int softcoreScore, final int unreadMessages) {
android.util.Log.i("Achievements", "Login success: " + username + " (Score: " + score + ")");
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("✓ Logged in as %s\nScore: %d pts (softcore: %d pts)\nUnread messages: %d",
username, score, softcoreScore, unreadMessages);
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
});
}
/**
* Called from native code when a challenge indicator should be shown.
* @param achievementTitle Achievement title
*/
public static void onChallengeIndicatorShow(final String achievementTitle) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("⚡ Challenge: %s", achievementTitle);
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
});
}
/**
* Called from native code when progress towards an achievement is made.
* @param achievementTitle Achievement title
* @param progress Progress string (e.g., "50%")
*/
public static void onProgressIndicatorUpdate(final String achievementTitle, final String progress) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message = String.format("📈 %s: %s", achievementTitle, progress);
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
});
}
/**
* Called from native code when game summary is available.
* @param gameTitle Game title
* @param unlockedCount Unlocked achievements
* @param totalCount Total achievements
* @param earnedPoints Earned points
* @param totalPoints Total points
* @param isHardcore Whether hardcore mode is active
*/
public static void onGameSummary(final String gameTitle, final int unlockedCount,
final int totalCount, final int earnedPoints,
final int totalPoints, final boolean isHardcore) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
String message;
if (totalCount > 0) {
message = String.format("%s%s\nUnlocked %d of %d achievements\nEarned %d of %d points",
gameTitle, isHardcore ? " (Hardcore)" : "",
unlockedCount, totalCount, earnedPoints, totalPoints);
} else {
message = String.format("%s\nThis game has no achievements.", gameTitle);
}
Toast.makeText(activity, message, Toast.LENGTH_LONG).show();
});
}
/**
* Called from native code to show a generic notification.
* @param message Message to display
* @param duration Duration (0 = SHORT, 1 = LONG)
*/
public static void showNotification(final String message, final int duration) {
sMainHandler.post(() -> {
Activity activity = getActivity();
if (activity == null) return;
int toastDuration = duration == 0 ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG;
Toast.makeText(activity, message, toastDuration).show();
});
}
private static Activity getActivity() {
return sActivity != null ? sActivity.get() : null;
}
}
@@ -168,6 +168,19 @@ public class SettingsDialogFragment extends DialogFragment {
controllerDialog.show(getParentFragmentManager(), "controller_test");
});
}
// RetroAchievements button
View btnAchievements = view.findViewById(R.id.btn_achievements);
android.util.Log.d("SettingsDialog", "btnAchievements found: " + (btnAchievements != null));
if (btnAchievements != null) {
btnAchievements.setOnClickListener(v -> {
android.util.Log.d("SettingsDialog", "Achievements button clicked!");
AchievementsDialogFragment achievementsDialog = AchievementsDialogFragment.newInstance();
achievementsDialog.show(getParentFragmentManager(), "achievements");
});
} else {
android.util.Log.e("SettingsDialog", "btn_achievements NOT FOUND in layout!");
}
// Populate scale spinner (1x..8x)
ArrayAdapter<CharSequence> scaleAdapter = ArrayAdapter.createFromResource(ctx,

Some files were not shown because too many files have changed in this diff Show More