diff --git a/Assets/dll/librcheevos.dll b/Assets/dll/librcheevos.dll index 578e6cf83f..106c20eb85 100644 Binary files a/Assets/dll/librcheevos.dll and b/Assets/dll/librcheevos.dll differ diff --git a/Assets/dll/librcheevos.so b/Assets/dll/librcheevos.so index 6477f4e879..863434025b 100644 Binary files a/Assets/dll/librcheevos.so and b/Assets/dll/librcheevos.so differ diff --git a/ExternalProjects/librcheevos/CMakeLists.txt b/ExternalProjects/librcheevos/CMakeLists.txt index e0a3690d7a..ff09059eee 100644 --- a/ExternalProjects/librcheevos/CMakeLists.txt +++ b/ExternalProjects/librcheevos/CMakeLists.txt @@ -134,3 +134,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} ARGS -E copy $ ${CMAKE_SOURCE_DIR}/../../output/dll ) + +if(MSVC) + set(PDB_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../../output/dll) +endif() diff --git a/ExternalProjects/librcheevos/rcheevos b/ExternalProjects/librcheevos/rcheevos index f0a5f96e10..c5304a61bc 160000 --- a/ExternalProjects/librcheevos/rcheevos +++ b/ExternalProjects/librcheevos/rcheevos @@ -1 +1 @@ -Subproject commit f0a5f96e10c2c6167af8bda24fdbf2c23ecaa01d +Subproject commit c5304a61bcf256ae80fcd1c8f64ad9646aaea757 diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs index e070ab0867..2162748881 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/LibRCheevos.cs @@ -1,16 +1,16 @@ using System; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Windows.Forms; using BizHawk.BizInvoke; using BizHawk.Common; -using BizHawk.Client.Common; -using BizHawk.Emulation.Common; #pragma warning disable IDE1006 // Naming Styles +// ReSharper disable UnusedMember.Global +// ReSharper disable EnumUnderlyingTypeIsInt +// not sure about these wrt marshalling, so ignore for now +// ReSharper disable FieldCanBeMadeReadOnly.Global + +// TODO: Make these record structs namespace BizHawk.Client.EmuHawk { @@ -104,12 +104,12 @@ namespace BizHawk.Client.EmuHawk } [StructLayout(LayoutKind.Sequential)] - public unsafe struct rc_api_buffer_t + public struct rc_api_buffer_t { public IntPtr write; public IntPtr end; public IntPtr next; - public fixed byte data[256]; + public unsafe fixed byte data[256]; } [StructLayout(LayoutKind.Sequential)] @@ -165,25 +165,31 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_fetch_user_unlocks_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int game_id; - public int hardcore; + [MarshalAs(UnmanagedType.Bool)] + public bool hardcore; public rc_api_fetch_user_unlocks_request_t(string username, string api_token, int game_id, bool hardcore) { this.username = username; this.api_token = api_token; this.game_id = game_id; - this.hardcore = hardcore ? 1 : 0; + this.hardcore = hardcore; } } [StructLayout(LayoutKind.Sequential)] public struct rc_api_login_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string password; public rc_api_login_request_t(string username, string api_token, string password) @@ -197,7 +203,9 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_start_session_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int game_id; @@ -302,10 +310,14 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_award_achievement_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int achievement_id; - public int hardcore; + [MarshalAs(UnmanagedType.Bool)] + public bool hardcore; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string game_hash; public rc_api_award_achievement_request_t(string username, string api_token, int achievement_id, bool hardcore, string game_hash) @@ -313,7 +325,7 @@ namespace BizHawk.Client.EmuHawk this.username = username; this.api_token = api_token; this.achievement_id = achievement_id; - this.hardcore = hardcore ? 1 : 0; + this.hardcore = hardcore; this.game_hash = game_hash; } } @@ -321,7 +333,9 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_fetch_game_data_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int game_id; @@ -336,6 +350,7 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_fetch_image_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string image_name; public rc_api_image_type_t image_type; @@ -349,9 +364,12 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_ping_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int game_id; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string rich_presence; public rc_api_ping_request_t(string username, string api_token, int game_id, string rich_presence) @@ -366,8 +384,11 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_resolve_hash_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; // note: not actually used + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; // note: not actually used + [MarshalAs(UnmanagedType.LPUTF8Str)] public string game_hash; public rc_api_resolve_hash_request_t(string username, string api_token, string game_hash) @@ -381,10 +402,13 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_submit_lboard_entry_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int leaderboard_id; public int score; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string game_hash; public rc_api_submit_lboard_entry_request_t(string username, string api_token, int leaderboard_id, int score, string game_hash) @@ -442,7 +466,9 @@ namespace BizHawk.Client.EmuHawk [StructLayout(LayoutKind.Sequential)] public struct rc_api_fetch_achievement_info_request_t { + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string api_token; public int achievement_id; public int first_entry; @@ -477,6 +503,7 @@ namespace BizHawk.Client.EmuHawk public int leaderboard_id; public int count; public int first_entry; + [MarshalAs(UnmanagedType.LPUTF8Str)] public string username; public rc_api_fetch_leaderboard_info_request_t(int leaderboard_id, int count, int first_entry, string username) diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs index 2086b8eed7..bef0e208d6 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Achievements.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Drawing; using System.Text; using System.Threading.Tasks; @@ -9,6 +10,37 @@ namespace BizHawk.Client.EmuHawk { private readonly RCheevosAchievementListForm _cheevoListForm = new(); + private class CheevoUnlockTask + { + private LibRCheevos.rc_api_award_achievement_request_t _apiParams; + public Task Task { get; private set; } + public bool Success { get; private set; } + + private void CheevoUnlockTaskCallback(byte[] serv_resp) + { + var res = _lib.rc_api_process_award_achievement_response(out var resp, serv_resp); + _lib.rc_api_destroy_award_achievement_response(ref resp); + Success = res == LibRCheevos.rc_error_t.RC_OK; + } + + public void DoRequest() + { + var res = _lib.rc_api_init_award_achievement_request(out var api_req, ref _apiParams); + Task = SendAPIRequestIfOK(res, ref api_req, CheevoUnlockTaskCallback); + } + + public CheevoUnlockTask(string username, string api_token, int achievement_id, bool hardcore, string game_hash) + { + _apiParams = new(username, api_token, achievement_id, hardcore, game_hash); + DoRequest(); + } + } + + // keep a list of all cheevo unlock trigger tasks that have been queued + // on Dispose(), we wait for all these to complete + // on Update(), we clear out successfully completed tasks, any not completed will be resent + private readonly List _queuedCheevoUnlockTasks = new(); + private bool CheevosActive { get; set; } private bool AllowUnofficialCheevos { get; set; } @@ -51,10 +83,10 @@ namespace BizHawk.Client.EmuHawk public bool IsEnabled => !Invalid && (IsOfficial || AllowUnofficialCheevos()); public bool IsOfficial => Category is LibRCheevos.rc_runtime_achievement_category_t.RC_ACHIEVEMENT_CATEGORY_CORE; - public async void LoadImages() + public void LoadImages() { - BadgeUnlocked = await GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT).ConfigureAwait(false); - BadgeLocked = await GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED).ConfigureAwait(false); + GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT, badge => BadgeUnlocked = badge); + GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, badge => BadgeLocked = badge); } public Cheevo(in LibRCheevos.rc_api_achievement_definition_t cheevo, Func allowUnofficialCheevos) @@ -116,8 +148,7 @@ namespace BizHawk.Client.EmuHawk return; } - var initReady = HardcoreMode ? _gameData.HardcoreInitUnlocksReady : _gameData.SoftcoreInitUnlocksReady; - initReady.WaitOne(); + _activeModeUnlocksTask.Wait(); foreach (var cheevo in _gameData.CheevoEnumerable) { @@ -142,7 +173,7 @@ namespace BizHawk.Client.EmuHawk { if (_gameData == null || _gameData.GameID == 0) return; - _gameData.SoftcoreInitUnlocksReady.WaitOne(); + _inactiveModeUnlocksTask.Wait(); foreach (var cheevo in _gameData.CheevoEnumerable) { @@ -152,7 +183,7 @@ namespace BizHawk.Client.EmuHawk } } - _gameData.HardcoreInitUnlocksReady.WaitOne(); + _activeModeUnlocksTask.Wait(); foreach (var cheevo in _gameData.CheevoEnumerable) { @@ -164,27 +195,5 @@ namespace BizHawk.Client.EmuHawk Update(); } - - private static async Task SendUnlockAchievementAsync(string username, string api_token, int id, bool hardcore, string hash) - { - var api_params = new LibRCheevos.rc_api_award_achievement_request_t(username, api_token, id, hardcore, hash); - var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; - if (_lib.rc_api_init_award_achievement_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) - { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - res = _lib.rc_api_process_award_achievement_response(out var resp, serv_req); - _lib.rc_api_destroy_award_achievement_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); - - if (res != LibRCheevos.rc_error_t.RC_OK) - { - // todo: warn user? correct local version of cheevos? - } - } - - private static async void SendUnlockAchievement(string username, string api_token, int id, bool hardcore, string hash) - => await SendUnlockAchievementAsync(username, api_token, id, hardcore, hash).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs index 531833f2bb..c03a27fddf 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.GameInfo.cs @@ -4,7 +4,6 @@ using System.Drawing; using System.IO; using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; namespace BizHawk.Client.EmuHawk @@ -21,6 +20,8 @@ namespace BizHawk.Client.EmuHawk private GameData _gameData; private readonly Dictionary _cachedGameDatas = new(); // keep game data around to avoid unneeded API calls for a simple RebootCore + private Task _activeModeUnlocksTask, _inactiveModeUnlocksTask; + public class GameData { public int GameID { get; } @@ -38,17 +39,14 @@ namespace BizHawk.Client.EmuHawk public Cheevo GetCheevoById(int i) => _cheevos[i]; public LBoard GetLboardById(int i) => _lboards[i]; - - public ManualResetEvent SoftcoreInitUnlocksReady { get; } - public ManualResetEvent HardcoreInitUnlocksReady { get; } - public async Task InitUnlocks(string username, string api_token, bool hardcore) + public Task InitUnlocks(string username, string api_token, bool hardcore, Action callback = null) { var api_params = new LibRCheevos.rc_api_fetch_user_unlocks_request_t(username, api_token, GameID, hardcore); - if (_lib.rc_api_init_fetch_user_unlocks_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var res = _lib.rc_api_init_fetch_user_unlocks_request(out var api_req, ref api_params); + return SendAPIRequestIfOK(res, ref api_req, serv_resp => { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - if (_lib.rc_api_process_fetch_user_unlocks_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + if (_lib.rc_api_process_fetch_user_unlocks_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK) { unsafe { @@ -61,26 +59,17 @@ namespace BizHawk.Client.EmuHawk } } } + + _lib.rc_api_destroy_fetch_user_unlocks_response(ref resp); } - _lib.rc_api_destroy_fetch_user_unlocks_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); - - if (hardcore) - { - HardcoreInitUnlocksReady?.Set(); - } - else - { - SoftcoreInitUnlocksReady?.Set(); - } + callback?.Invoke(); + }); } - public async Task LoadImages() + public void LoadImages() { - GameBadge = await GetImage(ImageName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_GAME).ConfigureAwait(false); + GetImage(ImageName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_GAME, badge => GameBadge = badge); if (_cheevos is null) return; @@ -91,7 +80,7 @@ namespace BizHawk.Client.EmuHawk } public int TotalCheevoPoints(bool hardcore) - => _cheevos?.Values.Sum(c => (c.IsEnabled && !c.Invalid && c.IsUnlocked(hardcore)) ? c.Points : 0) ?? 0; + => _cheevos?.Values.Sum(c => c.IsEnabled && !c.Invalid && c.IsUnlocked(hardcore) ? c.Points : 0) ?? 0; public unsafe GameData(in LibRCheevos.rc_api_fetch_game_data_response_t resp, Func allowUnofficialCheevos) { @@ -119,9 +108,6 @@ namespace BizHawk.Client.EmuHawk } _lboards = lboards; - - SoftcoreInitUnlocksReady = new(false); - HardcoreInitUnlocksReady = new(false); } public GameData(GameData gameData, Func allowUnofficialCheevos) @@ -135,9 +121,6 @@ namespace BizHawk.Client.EmuHawk _cheevos = gameData.CheevoEnumerable.ToDictionary(cheevo => cheevo.ID, cheevo => new(in cheevo, allowUnofficialCheevos)); _lboards = gameData.LBoardEnumerable.ToDictionary(lboard => lboard.ID, lboard => new(in lboard)); - - SoftcoreInitUnlocksReady = new(false); - HardcoreInitUnlocksReady = new(false); } public GameData() @@ -146,22 +129,21 @@ namespace BizHawk.Client.EmuHawk } } - private static async Task SendHashAsync(string hash) + private static int SendHash(string hash) { var api_params = new LibRCheevos.rc_api_resolve_hash_request_t(null, null, hash); var ret = 0; - if (_lib.rc_api_init_resolve_hash_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var res = _lib.rc_api_init_resolve_hash_request(out var api_req, ref api_params); + SendAPIRequestIfOK(res, ref api_req, serv_resp => { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - if (_lib.rc_api_process_resolve_hash_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + if (_lib.rc_api_process_resolve_hash_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK) { ret = resp.game_id; } _lib.rc_api_destroy_resolve_hash_response(ref resp); - } + }).Wait(); // currently, this is done synchronously - _lib.rc_api_destroy_request(ref api_req); return ret; } @@ -174,7 +156,7 @@ namespace BizHawk.Client.EmuHawk return _cachedGameIds[hash]; } - var ret = SendHashAsync(hash).ConfigureAwait(false).GetAwaiter().GetResult(); + var ret = SendHash(hash); _cachedGameIds.Add(hash, ret); return ret; } @@ -191,56 +173,75 @@ namespace BizHawk.Client.EmuHawk return 0; } - private static async Task InitGameDataAsync(GameData gameData, string username, string api_token, bool hardcore) + private void InitGameData() { - await gameData.InitUnlocks(username, api_token, hardcore).ConfigureAwait(false); - await gameData.InitUnlocks(username, api_token, !hardcore).ConfigureAwait(false); - await gameData.LoadImages().ConfigureAwait(false); - } + _activeModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, HardcoreMode, () => + { + foreach (var cheevo in _gameData.CheevoEnumerable) + { + if (cheevo.IsEnabled && !cheevo.IsUnlocked(HardcoreMode)) + { + _lib.rc_runtime_activate_achievement(ref _runtime, cheevo.ID, cheevo.Definition, IntPtr.Zero, 0); + } + } + }); - private static async void InitGameData(GameData gameData, string username, string api_token, bool hardcore) - => await InitGameDataAsync(gameData, username, api_token, hardcore); + _inactiveModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, !HardcoreMode); + _gameData.LoadImages(); + + foreach (var lboard in _gameData.LBoardEnumerable) + { + _lib.rc_runtime_activate_lboard(ref _runtime, lboard.ID, lboard.Definition, IntPtr.Zero, 0); + } + + if (_gameData.RichPresenseScript is not null) + { + _lib.rc_runtime_activate_richpresence(ref _runtime, _gameData.RichPresenseScript, IntPtr.Zero, 0); + } + } private static GameData GetGameData(string username, string api_token, int id, Func allowUnofficialCheevos) { var api_params = new LibRCheevos.rc_api_fetch_game_data_request_t(username, api_token, id); var ret = new GameData(); - if (_lib.rc_api_init_fetch_game_data_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var res = _lib.rc_api_init_fetch_game_data_request(out var api_req, ref api_params); + SendAPIRequestIfOK(res, ref api_req, serv_resp => { - var serv_req = SendAPIRequest(in api_req).ConfigureAwait(false).GetAwaiter().GetResult(); - if (_lib.rc_api_process_fetch_game_data_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + if (_lib.rc_api_process_fetch_game_data_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK) { ret = new(in resp, allowUnofficialCheevos); } _lib.rc_api_destroy_fetch_game_data_response(ref resp); - } + }).Wait(); - _lib.rc_api_destroy_request(ref api_req); return ret; } - private static async Task GetImage(string image_name, LibRCheevos.rc_api_image_type_t image_type) + private static void GetImage(string image_name, LibRCheevos.rc_api_image_type_t image_type, Action callback) { - if (image_name is null) return null; + if (image_name is null) + { + callback(null); + return; + } var api_params = new LibRCheevos.rc_api_fetch_image_request_t(image_name, image_type); - Bitmap ret = null; - if (_lib.rc_api_init_fetch_image_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var res = _lib.rc_api_init_fetch_image_request(out var api_req, ref api_params); + SendAPIRequestIfOK(res, ref api_req, serv_resp => { + Bitmap image; try { - var serv_resp = await SendAPIRequest(in api_req).ConfigureAwait(false); - ret = new(new MemoryStream(serv_resp)); + image = new(new MemoryStream(serv_resp)); } catch { - ret = null; + image = null; } - } - _lib.rc_api_destroy_request(ref api_req); - return ret; + callback(image); + }); } } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs index 84138d4286..bb9273c8fb 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Http.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.Net.Http; +using System.Text; +using System.Threading; using System.Threading.Tasks; namespace BizHawk.Client.EmuHawk @@ -20,24 +23,43 @@ namespace BizHawk.Client.EmuHawk private static async Task HttpPost(string url, string post) { - HttpResponseMessage response; try { - response = await _http.PostAsync(url + "?" + post, null).ConfigureAwait(false); + using var content = new StringContent(post, Encoding.UTF8, "application/x-www-form-urlencoded"); + using var response = await _http.PostAsync(url, content).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + return new byte[1]; + } + return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } catch (Exception e) { Console.WriteLine(e); return new byte[1]; } - if (!response.IsSuccessStatusCode) - { - return new byte[1]; - } - return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); } - private static Task SendAPIRequest(in LibRCheevos.rc_api_request_t api_req) - => api_req.post_data != IntPtr.Zero ? HttpPost(api_req.URL, api_req.PostData) : HttpGet(api_req.URL); + private static Task SendAPIRequest(in LibRCheevos.rc_api_request_t api_req, Action callback) + { + var isPost = api_req.post_data != IntPtr.Zero; + var url = api_req.URL; + var postData = isPost ? api_req.PostData : null; + return Task.Factory.StartNew(() => + { + var apiRequestTask = isPost ? HttpPost(url, postData) : HttpGet(url); + callback(apiRequestTask.Result); + }, TaskCreationOptions.RunContinuationsAsynchronously); + } + + private static Task SendAPIRequestIfOK(LibRCheevos.rc_error_t res, ref LibRCheevos.rc_api_request_t api_req, Action callback) + { + var ret = res == LibRCheevos.rc_error_t.RC_OK + ? SendAPIRequest(in api_req, callback) + : Task.CompletedTask; + _lib.rc_api_destroy_request(ref api_req); + // TODO: report failures when res is not RC_OK (can be done in this function, as it's the main thread) + return ret; + } } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs index 7c44d35790..86b9f39fa1 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Leaderboards.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text; using System.Threading.Tasks; @@ -7,6 +8,37 @@ namespace BizHawk.Client.EmuHawk { private readonly RCheevosLeaderboardListForm _lboardListForm = new(); + private class LboardTriggerTask + { + private LibRCheevos.rc_api_submit_lboard_entry_request_t _apiParams; + public Task Task { get; private set; } + public bool Success { get; private set; } + + private void LboardTriggerTaskCallback(byte[] serv_resp) + { + var res = _lib.rc_api_process_submit_lboard_entry_response(out var resp, serv_resp); + _lib.rc_api_destroy_submit_lboard_entry_response(ref resp); + Success = res == LibRCheevos.rc_error_t.RC_OK; + } + + public void DoRequest() + { + var res = _lib.rc_api_init_submit_lboard_entry_request(out var api_req, ref _apiParams); + Task = SendAPIRequestIfOK(res, ref api_req, LboardTriggerTaskCallback); + } + + public LboardTriggerTask(string username, string api_token, int id, int value, string hash) + { + _apiParams = new(username, api_token, id, value, hash); + DoRequest(); + } + } + + // keep a list of all cheevo unlock trigger tasks that have been queued + // on Dispose(), we wait for all these to complete + // on Update(), we clear out successfully completed tasks, any not completed will be resent + private readonly List _queuedLboardTriggerTasks = new(); + private bool LBoardsActive { get; set; } private LBoard CurrentLboard { get; set; } @@ -57,27 +89,5 @@ namespace BizHawk.Client.EmuHawk SetScore(0); } } - - private static async Task SendTriggerLeaderboardAsync(string username, string api_token, int id, int value, string hash) - { - var api_params = new LibRCheevos.rc_api_submit_lboard_entry_request_t(username, api_token, id, value, hash); - var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; - if (_lib.rc_api_init_submit_lboard_entry_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) - { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - res = _lib.rc_api_process_submit_lboard_entry_response(out var resp, serv_req); - _lib.rc_api_destroy_submit_lboard_entry_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); - - if (res != LibRCheevos.rc_error_t.RC_OK) - { - // todo: warn user? - } - } - - private static async void SendTriggerLeaderboard(string username, string api_token, int id, int value, string hash) - => await SendTriggerLeaderboardAsync(username, api_token, id, value, hash).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs index 31bcc47658..a2453fabf2 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Login.cs @@ -1,6 +1,4 @@ using System; -using System.Threading; -using System.Threading.Tasks; namespace BizHawk.Client.EmuHawk { @@ -9,34 +7,30 @@ namespace BizHawk.Client.EmuHawk private string Username, ApiToken; private bool LoggedIn => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(ApiToken); - private ManualResetEvent InitLoginDone { get; } - private event Action LoginStatusChanged; - private async Task LoginCallback(string username, string password) + private bool DoLogin(string username, string apiToken = null, string password = null) { Username = null; ApiToken = null; - var api_params = new LibRCheevos.rc_api_login_request_t(username, null, password); - if (_lib.rc_api_init_login_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var api_params = new LibRCheevos.rc_api_login_request_t(username, apiToken, password); + var res = _lib.rc_api_init_login_request(out var api_req, ref api_params); + SendAPIRequestIfOK(res, ref api_req, serv_resp => { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - if (_lib.rc_api_process_login_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) + if (_lib.rc_api_process_login_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK) { Username = resp.Username; ApiToken = resp.ApiToken; } _lib.rc_api_destroy_login_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); + }).Wait(); // currently, this is done synchronously return LoggedIn; } - private async void Login() + private void Login() { var config = _getConfig(); Username = config.RAUsername; @@ -45,41 +39,20 @@ namespace BizHawk.Client.EmuHawk if (LoggedIn) { // OK, Username and ApiToken are probably valid, let's ensure they are now - var api_params = new LibRCheevos.rc_api_login_request_t(Username, ApiToken, null); - - Username = null; - ApiToken = null; - - if (_lib.rc_api_init_login_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + if (DoLogin(Username, apiToken: ApiToken)) { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - if (_lib.rc_api_process_login_response(out var resp, serv_req) == LibRCheevos.rc_error_t.RC_OK) - { - Username = resp.Username; - ApiToken = resp.ApiToken; - } - - _lib.rc_api_destroy_login_response(ref resp); + config.RAUsername = Username; + config.RAToken = ApiToken; + if (EnableSoundEffects) _loginSound.PlayNoExceptions(); + return; } - - _lib.rc_api_destroy_request(ref api_req); } - if (LoggedIn) - { - config.RAUsername = Username; - config.RAToken = ApiToken; - InitLoginDone.Set(); - if (EnableSoundEffects) _loginSound.PlayNoExceptions(); - return; - } - - using var loginForm = new RCheevosLoginForm(LoginCallback); + using var loginForm = new RCheevosLoginForm((username, password) => DoLogin(username, password: password)); loginForm.ShowDialog(); - + config.RAUsername = Username; config.RAToken = ApiToken; - InitLoginDone.Set(); if (LoggedIn && EnableSoundEffects) { diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs index b86a0ea7d6..e6d635409e 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.Ping.cs @@ -8,42 +8,33 @@ namespace BizHawk.Client.EmuHawk { private bool RichPresenceActive { get; set; } private string CurrentRichPresence { get; set; } + private bool GameSessionStartSuccessful { get; set; } - private static async Task StartGameSessionAsync(string username, string api_token, int id) + private Task _startGameSessionTask; + + private void StartGameSession() { - var api_params = new LibRCheevos.rc_api_start_session_request_t(username, api_token, id); - var res = LibRCheevos.rc_error_t.RC_INVALID_STATE; - if (_lib.rc_api_init_start_session_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + GameSessionStartSuccessful = false; + var api_params = new LibRCheevos.rc_api_start_session_request_t(Username, ApiToken, _gameData.GameID); + var res = _lib.rc_api_init_start_session_request(out var api_req, ref api_params); + _startGameSessionTask = SendAPIRequestIfOK(res, ref api_req, serv_resp => { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - res = _lib.rc_api_process_start_session_response(out var resp, serv_req); + GameSessionStartSuccessful = _lib.rc_api_process_start_session_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK; _lib.rc_api_destroy_start_session_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); - return res == LibRCheevos.rc_error_t.RC_OK; + }); } - // todo: warn on failure? - private static async void StartGameSession(string username, string api_token, int id) - => await StartGameSessionAsync(username, api_token, id).ConfigureAwait(false); - - private static async Task SendPingAsync(string username, string api_token, int id, string rich_presence) + private static void SendPing(string username, string api_token, int id, string rich_presence) { var api_params = new LibRCheevos.rc_api_ping_request_t(username, api_token, id, rich_presence); - if (_lib.rc_api_init_ping_request(out var api_req, ref api_params) == LibRCheevos.rc_error_t.RC_OK) + var res = _lib.rc_api_init_ping_request(out var api_req, ref api_params); + SendAPIRequestIfOK(res, ref api_req, static serv_resp => { - var serv_req = await SendAPIRequest(in api_req).ConfigureAwait(false); - _lib.rc_api_process_ping_response(out var resp, serv_req); + _lib.rc_api_process_ping_response(out var resp, serv_resp); _lib.rc_api_destroy_ping_response(ref resp); - } - - _lib.rc_api_destroy_request(ref api_req); + }); } - private static async void SendPing(string username, string api_token, int id, string rich_presence) - => await SendPingAsync(username, api_token, id, rich_presence).ConfigureAwait(false); - private readonly byte[] _richPresenceBuffer = new byte[1024]; private DateTime _lastPingTime = DateTime.Now; diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs index efe4dd3f20..f6029b36f6 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevos.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using System.Windows.Forms; using BizHawk.BizInvoke; @@ -194,9 +195,7 @@ namespace BizHawk.Client.EmuHawk { _runtime = default; _lib.rc_runtime_init(ref _runtime); - InitLoginDone = new(false); Login(); - InitLoginDone.WaitOne(); _eventcb = EventHandlerCallback; _peekcb = PeekCallback; @@ -214,6 +213,8 @@ namespace BizHawk.Client.EmuHawk public override void Dispose() { + Task.WaitAll(_queuedCheevoUnlockTasks.Select(t => t.Task).ToArray()); + Task.WaitAll(_queuedLboardTriggerTasks.Select(t => t.Task).ToArray()); _lib.rc_runtime_destroy(ref _runtime); Stop(); _gameInfoForm.Dispose(); @@ -229,6 +230,7 @@ namespace BizHawk.Client.EmuHawk return; } + _activeModeUnlocksTask.Wait(); var size = _lib.rc_runtime_progress_size(ref _runtime, IntPtr.Zero); if (size > 0) { @@ -251,6 +253,7 @@ namespace BizHawk.Client.EmuHawk HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode."); } + _activeModeUnlocksTask.Wait(); _lib.rc_runtime_reset(ref _runtime); if (!File.Exists(path + ".rap")) return; @@ -323,7 +326,7 @@ namespace BizHawk.Client.EmuHawk { if (memFunctions.ReadFunc is not null) { - for (int i = 0; i < memFunctions.BankSize; i++) + for (var i = 0; i < memFunctions.BankSize; i++) { _readMap.Add(addr + i, (memFunctions.ReadFunc, addr)); } @@ -351,43 +354,23 @@ namespace BizHawk.Client.EmuHawk ? new(cachedGameData, () => AllowUnofficialCheevos) : GetGameData(Username, ApiToken, gameId, () => AllowUnofficialCheevos); - StartGameSession(Username, ApiToken, gameId); + StartGameSession(); _cachedGameDatas.Remove(gameId); _cachedGameDatas.Add(gameId, _gameData); - InitGameData(_gameData, Username, ApiToken, HardcoreMode); - - foreach (var lboard in _gameData.LBoardEnumerable) - { - _lib.rc_runtime_activate_lboard(ref _runtime, lboard.ID, lboard.Definition, IntPtr.Zero, 0); - } - - if (_gameData.RichPresenseScript is not null) - { - _lib.rc_runtime_activate_richpresence(ref _runtime, _gameData.RichPresenseScript, IntPtr.Zero, 0); - } - - var waitInit = HardcoreMode ? _gameData.HardcoreInitUnlocksReady : _gameData.SoftcoreInitUnlocksReady; - // hopefully not too long, given we spent some time doing other work - waitInit.WaitOne(); - - foreach (var cheevo in _gameData.CheevoEnumerable) - { - if (cheevo.IsEnabled && !cheevo.IsUnlocked(HardcoreMode)) - { - _lib.rc_runtime_activate_achievement(ref _runtime, cheevo.ID, cheevo.Definition, IntPtr.Zero, 0); - } - } + InitGameData(); } else { _gameData = new(); + _activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask; } } else { _gameData = new(); + _activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask; } // validate addresses now that we have cheevos init @@ -412,6 +395,26 @@ namespace BizHawk.Client.EmuHawk return; } + if (_startGameSessionTask is not null && _startGameSessionTask.IsCompleted && !GameSessionStartSuccessful) + { + // retry if this failed + StartGameSession(); + } + + _queuedCheevoUnlockTasks.RemoveAll(t => t.Task.IsCompleted && t.Success); + + foreach (var task in _queuedCheevoUnlockTasks.Where(task => task.Task.IsCompleted && !task.Success)) + { + task.DoRequest(); + } + + _queuedLboardTriggerTasks.RemoveAll(t => t.Task.IsCompleted && t.Success); + + foreach (var task in _queuedLboardTriggerTasks.Where(task => task.Task.IsCompleted && !task.Success)) + { + task.DoRequest(); + } + if (HardcoreMode) { CheckHardcoreModeConditions(); @@ -445,7 +448,7 @@ namespace BizHawk.Client.EmuHawk if (cheevo.IsOfficial) { - SendUnlockAchievement(Username, ApiToken, evt->id, HardcoreMode, _gameHash); + _queuedCheevoUnlockTasks.Add(new(Username, ApiToken, evt->id, HardcoreMode, _gameHash)); } } @@ -530,7 +533,7 @@ namespace BizHawk.Client.EmuHawk var lboard = _gameData.GetLboardById(evt->id); if (!lboard.Invalid) { - SendTriggerLeaderboard(Username, ApiToken, evt->id, evt->value, _gameHash); + _queuedLboardTriggerTasks.Add(new(Username, ApiToken, evt->id, evt->value, _gameHash)); if (!lboard.Hidden) { @@ -592,7 +595,7 @@ namespace BizHawk.Client.EmuHawk public override void OnFrameAdvance() { - if (!LoggedIn) + if (!LoggedIn || !_activeModeUnlocksTask.IsCompleted) { return; } diff --git a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs index 9f5719f672..c9c061ca96 100644 --- a/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs +++ b/src/BizHawk.Client.EmuHawk/RetroAchievements/RCheevosLoginForm.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Threading.Tasks; using System.Windows.Forms; namespace BizHawk.Client.EmuHawk @@ -10,17 +9,17 @@ namespace BizHawk.Client.EmuHawk /// public partial class RCheevosLoginForm : Form { - public RCheevosLoginForm(Func> loginCallback) + public RCheevosLoginForm(Func loginCallback) { InitializeComponent(); _loginCallback = loginCallback; } - private readonly Func> _loginCallback; + private readonly Func _loginCallback; - private async void btnLogin_Click(object sender, EventArgs e) + private void btnLogin_Click(object sender, EventArgs e) { - var res = await _loginCallback(txtUsername.Text, txtPassword.Text); + var res = _loginCallback(txtUsername.Text, txtPassword.Text); if (res) { MessageBox.Show("Login successful");