Fix RetroAchievements rich presence not working

Cleanup this code so it plays nicer with BizHawk's "run everything on the main thread" (+ WinForms not playing nice with async methods)
Resend any game session start / achievement unlocks / leaderboard triggers if they failed. Wait for all achievement unlocks and leaderboard triggers to finish on Dispose() (mostly for catching them when user closes BizHawk)
Update rcheevos to 10.7.0
This commit is contained in:
CasualPokePlayer 2023-04-18 21:29:05 -07:00
parent 988ae27292
commit 4268c50617
13 changed files with 271 additions and 232 deletions

Binary file not shown.

Binary file not shown.

View File

@ -134,3 +134,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND}
ARGS -E copy $<TARGET_FILE:${RC_TARGET}> ${CMAKE_SOURCE_DIR}/../../output/dll
)
if(MSVC)
set(PDB_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../../output/dll)
endif()

@ -1 +1 @@
Subproject commit f0a5f96e10c2c6167af8bda24fdbf2c23ecaa01d
Subproject commit c5304a61bcf256ae80fcd1c8f64ad9646aaea757

View File

@ -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)

View File

@ -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<CheevoUnlockTask> _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<bool> 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);
}
}

View File

@ -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<int, GameData> _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<bool> allowUnofficialCheevos)
{
@ -119,9 +108,6 @@ namespace BizHawk.Client.EmuHawk
}
_lboards = lboards;
SoftcoreInitUnlocksReady = new(false);
HardcoreInitUnlocksReady = new(false);
}
public GameData(GameData gameData, Func<bool> allowUnofficialCheevos)
@ -135,9 +121,6 @@ namespace BizHawk.Client.EmuHawk
_cheevos = gameData.CheevoEnumerable.ToDictionary<Cheevo, int, Cheevo>(cheevo => cheevo.ID, cheevo => new(in cheevo, allowUnofficialCheevos));
_lboards = gameData.LBoardEnumerable.ToDictionary<LBoard, int, LBoard>(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<int> 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<bool> 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<Bitmap> 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<Bitmap> 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);
});
}
}
}

View File

@ -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<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> 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;
}
}
}

View File

@ -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<LboardTriggerTask> _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);
}
}

View File

@ -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<bool> 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)
{

View File

@ -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<bool> 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;

View File

@ -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;
}

View File

@ -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
/// </summary>
public partial class RCheevosLoginForm : Form
{
public RCheevosLoginForm(Func<string, string, Task<bool>> loginCallback)
public RCheevosLoginForm(Func<string, string, bool> loginCallback)
{
InitializeComponent();
_loginCallback = loginCallback;
}
private readonly Func<string, string, Task<bool>> _loginCallback;
private readonly Func<string, string, bool> _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");