mirror of
https://github.com/izzy2lost/PSX2.git
synced 2026-07-05 15:18:36 -07:00
Added RetroAchievements!
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user