Refactor rcheevo http requests (again). This should better protect against the UI thread deadlocking due to Task semantics with WinForms

This commit is contained in:
CasualPokePlayer 2023-05-01 03:48:30 -07:00
parent e065263ff2
commit b517228475
7 changed files with 526 additions and 226 deletions

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk
{
@ -10,37 +9,32 @@ namespace BizHawk.Client.EmuHawk
{
private readonly RCheevosAchievementListForm _cheevoListForm = new();
private class CheevoUnlockTask
private sealed class CheevoUnlockRequest : RCheevoHttpRequest
{
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)
protected override void ResponseCallback(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;
if (res != LibRCheevos.rc_error_t.RC_OK)
{
Console.WriteLine($"CheevoUnlockRequest failed in ResponseCallback with {res}");
}
}
public void DoRequest()
public override void DoRequest()
{
var res = _lib.rc_api_init_award_achievement_request(out var api_req, ref _apiParams);
Task = SendAPIRequestIfOK(res, ref api_req, CheevoUnlockTaskCallback);
var apiParamsResult = _lib.rc_api_init_award_achievement_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public CheevoUnlockTask(string username, string api_token, int achievement_id, bool hardcore, string game_hash)
public CheevoUnlockRequest(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; }
@ -54,8 +48,11 @@ namespace BizHawk.Client.EmuHawk
public string Definition { get; }
public string Author { get; }
private string BadgeName { get; }
public Bitmap BadgeUnlocked { get; private set; }
public Bitmap BadgeLocked { get; private set; }
public Bitmap BadgeUnlocked => _badgeUnlockedRequest?.Image;
public Bitmap BadgeLocked => _badgeLockedRequest?.Image;
private ImageRequest _badgeUnlockedRequest, _badgeLockedRequest;
public DateTime Created { get; }
public DateTime Updated { get; }
@ -83,10 +80,12 @@ 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 void LoadImages()
public void LoadImages(IList<RCheevoHttpRequest> requests)
{
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);
_badgeUnlockedRequest = new(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT);
_badgeLockedRequest = new(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED);
requests.Add(_badgeUnlockedRequest);
requests.Add(_badgeLockedRequest);
}
public Cheevo(in LibRCheevos.rc_api_achievement_definition_t cheevo, Func<bool> allowUnofficialCheevos)
@ -99,8 +98,6 @@ namespace BizHawk.Client.EmuHawk
Definition = cheevo.Definition;
Author = cheevo.Author;
BadgeName = cheevo.BadgeName;
BadgeUnlocked = null;
BadgeLocked = null;
Created = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.created).ToLocalTime();
Updated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.updated).ToLocalTime();
IsSoftcoreUnlocked = false;
@ -120,8 +117,6 @@ namespace BizHawk.Client.EmuHawk
Definition = cheevo.Definition;
Author = cheevo.Author;
BadgeName = cheevo.BadgeName;
BadgeUnlocked = null;
BadgeLocked = null;
Created = cheevo.Created;
Updated = cheevo.Updated;
IsSoftcoreUnlocked = false;
@ -148,7 +143,7 @@ namespace BizHawk.Client.EmuHawk
return;
}
_activeModeUnlocksTask.Wait();
_activeModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable)
{
@ -173,7 +168,7 @@ namespace BizHawk.Client.EmuHawk
{
if (_gameData == null || _gameData.GameID == 0) return;
_inactiveModeUnlocksTask.Wait();
_inactiveModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable)
{
@ -183,7 +178,7 @@ namespace BizHawk.Client.EmuHawk
}
}
_activeModeUnlocksTask.Wait();
_activeModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable)
{

View File

@ -4,7 +4,6 @@ using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BizHawk.Common.CollectionExtensions;
@ -22,7 +21,131 @@ 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 sealed class UserUnlocksRequest : RCheevoHttpRequest
{
private LibRCheevos.rc_api_fetch_user_unlocks_request_t _apiParams;
private readonly IReadOnlyDictionary<int, Cheevo> _cheevos;
private readonly Action _activeModeCallback;
protected override void ResponseCallback(byte[] serv_resp)
{
var res = _lib.rc_api_process_fetch_user_unlocks_response(out var resp, serv_resp);
if (res == LibRCheevos.rc_error_t.RC_OK)
{
unsafe
{
var unlocks = (int*)resp.achievement_ids;
for (var i = 0; i < resp.num_achievement_ids; i++)
{
if (_cheevos.TryGetValue(unlocks![i], out var cheevo))
{
cheevo.SetUnlocked(_apiParams.hardcore, true);
}
}
}
_activeModeCallback?.Invoke();
}
else
{
Console.WriteLine($"UserUnlocksRequest failed in ResponseCallback with {res}");
}
_lib.rc_api_destroy_fetch_user_unlocks_response(ref resp);
}
public override void DoRequest()
{
var apiParamsResult = _lib.rc_api_init_fetch_user_unlocks_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public UserUnlocksRequest(string username, string api_token, int game_id, bool hardcore,
IReadOnlyDictionary<int, Cheevo> cheevos, Action activeModeCallback = null)
{
_apiParams = new(username, api_token, game_id, hardcore);
_cheevos = cheevos;
_activeModeCallback = activeModeCallback;
}
}
private RCheevoHttpRequest _activeModeUnlocksRequest, _inactiveModeUnlocksRequest;
private sealed class GameDataRequest : RCheevoHttpRequest
{
private LibRCheevos.rc_api_fetch_game_data_request_t _apiParams;
private readonly Func<bool> _allowUnofficialCheevos;
public GameData GameData { get; private set; }
protected override void ResponseCallback(byte[] serv_resp)
{
var res = _lib.rc_api_process_fetch_game_data_response(out var resp, serv_resp);
if (res == LibRCheevos.rc_error_t.RC_OK)
{
GameData = new(in resp, _allowUnofficialCheevos);
}
else
{
Console.WriteLine($"GameDataRequest failed in ResponseCallback with {res}");
}
_lib.rc_api_destroy_fetch_game_data_response(ref resp);
}
public override void DoRequest()
{
GameData = new();
var apiParamsResult = _lib.rc_api_init_fetch_game_data_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public GameDataRequest(string username, string api_token, int game_id, Func<bool> allowUnofficialCheevos)
{
_apiParams = new(username, api_token, game_id);
_allowUnofficialCheevos = allowUnofficialCheevos;
}
}
private sealed class ImageRequest : RCheevoHttpRequest
{
private LibRCheevos.rc_api_fetch_image_request_t _apiParams;
public Bitmap Image { get; private set; }
public override bool ShouldRetry => false;
protected override void ResponseCallback(byte[] serv_resp)
{
try
{
var image = new Bitmap(new MemoryStream(serv_resp));
Image = image;
}
catch
{
Image = null;
}
}
public override void DoRequest()
{
Image = null;
if (_apiParams.image_name is null)
{
return;
}
var apiParamsResult = _lib.rc_api_init_fetch_image_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public ImageRequest(string image_name, LibRCheevos.rc_api_image_type_t image_type)
{
_apiParams = new(image_name, image_type);
}
}
public class GameData
{
@ -30,9 +153,11 @@ namespace BizHawk.Client.EmuHawk
public ConsoleID ConsoleID { get; }
public string Title { get; }
private string ImageName { get; }
public Bitmap GameBadge { get; private set; }
public Bitmap GameBadge => _gameBadgeImageRequest?.Image;
public string RichPresenseScript { get; }
private ImageRequest _gameBadgeImageRequest;
private readonly IReadOnlyDictionary<int, Cheevo> _cheevos;
private readonly IReadOnlyDictionary<int, LBoard> _lboards;
@ -42,43 +167,26 @@ namespace BizHawk.Client.EmuHawk
public Cheevo GetCheevoById(int i) => _cheevos[i];
public LBoard GetLboardById(int i) => _lboards[i];
public Task InitUnlocks(string username, string api_token, bool hardcore, Action callback = null)
public UserUnlocksRequest 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);
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 =>
{
if (_lib.rc_api_process_fetch_user_unlocks_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK)
{
unsafe
{
var unlocks = (int*)resp.achievement_ids;
for (var i = 0; i < resp.num_achievement_ids; i++)
{
if (_cheevos.TryGetValue(unlocks![i], out var cheevo))
{
cheevo.SetUnlocked(hardcore, true);
}
}
}
_lib.rc_api_destroy_fetch_user_unlocks_response(ref resp);
}
callback?.Invoke();
});
return new(username, api_token, GameID, hardcore, _cheevos, callback);
}
public void LoadImages()
public IEnumerable<RCheevoHttpRequest> LoadImages()
{
GetImage(ImageName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_GAME, badge => GameBadge = badge);
var requests = new List<RCheevoHttpRequest>(1 + (_cheevos?.Count ?? 0) * 2);
if (_cheevos is null) return;
_gameBadgeImageRequest = new(ImageName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_GAME);
requests.Add(_gameBadgeImageRequest);
if (_cheevos is null) return requests;
foreach (var cheevo in _cheevos.Values)
{
cheevo.LoadImages();
cheevo.LoadImages(requests);
}
return requests;
}
public int TotalCheevoPoints(bool hardcore)
@ -90,7 +198,6 @@ namespace BizHawk.Client.EmuHawk
ConsoleID = resp.console_id;
Title = resp.Title;
ImageName = resp.ImageName;
GameBadge = null;
RichPresenseScript = resp.RichPresenceScript;
var cheevos = new Dictionary<int, Cheevo>();
@ -118,7 +225,6 @@ namespace BizHawk.Client.EmuHawk
ConsoleID = gameData.ConsoleID;
Title = gameData.Title;
ImageName = gameData.ImageName;
GameBadge = null;
RichPresenseScript = gameData.RichPresenseScript;
_cheevos = gameData.CheevoEnumerable.ToDictionary<Cheevo, int, Cheevo>(cheevo => cheevo.ID, cheevo => new(in cheevo, allowUnofficialCheevos));
@ -131,22 +237,48 @@ namespace BizHawk.Client.EmuHawk
}
}
private static int SendHash(string hash)
private sealed class ResolveHashRequest : RCheevoHttpRequest
{
var api_params = new LibRCheevos.rc_api_resolve_hash_request_t(null, null, hash);
var ret = 0;
var res = _lib.rc_api_init_resolve_hash_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, serv_resp =>
private LibRCheevos.rc_api_resolve_hash_request_t _apiParams;
public int GameID { get; private set; }
// eh? not sure I want this retried, giving the blocking behavior
public override bool ShouldRetry => false;
protected override void ResponseCallback(byte[] serv_resp)
{
if (_lib.rc_api_process_resolve_hash_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK)
var res = _lib.rc_api_process_resolve_hash_response(out var resp, serv_resp);
if (res == LibRCheevos.rc_error_t.RC_OK)
{
ret = resp.game_id;
GameID = resp.game_id;
}
else
{
Console.WriteLine($"ResolveHashRequest failed in ResponseCallback with {res}");
}
_lib.rc_api_destroy_resolve_hash_response(ref resp);
}).Wait(); // currently, this is done synchronously
}
return ret;
public override void DoRequest()
{
GameID = 0;
var apiParamsResult = _lib.rc_api_init_resolve_hash_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public ResolveHashRequest(string hash)
{
_apiParams = new(null, null, hash);
}
}
private int SendHash(string hash)
{
var resolveHashRequest = new ResolveHashRequest(hash);
_inactiveHttpRequests.Push(resolveHashRequest);
resolveHashRequest.Wait(); // currently, this is done synchronously
return resolveHashRequest.GameID;
}
protected override int IdentifyHash(string hash)
@ -169,7 +301,7 @@ namespace BizHawk.Client.EmuHawk
private void InitGameData()
{
_activeModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, HardcoreMode, () =>
_activeModeUnlocksRequest = _gameData.InitUnlocks(Username, ApiToken, HardcoreMode, () =>
{
foreach (var cheevo in _gameData.CheevoEnumerable)
{
@ -179,9 +311,13 @@ namespace BizHawk.Client.EmuHawk
}
}
});
_inactiveHttpRequests.Push(_activeModeUnlocksRequest);
_inactiveModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, !HardcoreMode);
_gameData.LoadImages();
_inactiveModeUnlocksRequest = _gameData.InitUnlocks(Username, ApiToken, !HardcoreMode);
_inactiveHttpRequests.Push(_inactiveModeUnlocksRequest);
var loadImageRequests = _gameData.LoadImages();
_inactiveHttpRequests.PushRange(loadImageRequests.ToArray());
foreach (var lboard in _gameData.LBoardEnumerable)
{
@ -194,48 +330,12 @@ namespace BizHawk.Client.EmuHawk
}
}
private static GameData GetGameData(string username, string api_token, int id, Func<bool> allowUnofficialCheevos)
private GameData GetGameData(int id)
{
var api_params = new LibRCheevos.rc_api_fetch_game_data_request_t(username, api_token, id);
var ret = new GameData();
var res = _lib.rc_api_init_fetch_game_data_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, serv_resp =>
{
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();
return ret;
}
private static void GetImage(string image_name, LibRCheevos.rc_api_image_type_t image_type, Action<Bitmap> callback)
{
if (image_name is null)
{
callback(null);
return;
}
var api_params = new LibRCheevos.rc_api_fetch_image_request_t(image_name, image_type);
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
{
image = new(new MemoryStream(serv_resp));
}
catch
{
image = null;
}
callback(image);
});
var gameDataRequest = new GameDataRequest(Username, ApiToken, id, () => AllowUnofficialCheevos);
_inactiveHttpRequests.Push(gameDataRequest);
gameDataRequest.Wait();
return gameDataRequest.GameData;
}
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
@ -11,14 +13,188 @@ namespace BizHawk.Client.EmuHawk
{
private static readonly HttpClient _http = new() { DefaultRequestHeaders = { ConnectionClose = true } };
/// <summary>
/// A concurrent stack containing all pending HTTP requests
/// The main thread will push new requests onto this stack
/// The HTTP thread will pop requests and start them
/// </summary>
private readonly ConcurrentStack<RCheevoHttpRequest> _inactiveHttpRequests = new();
/// <summary>
/// A list containing all currently active HTTP requests
/// Completed requests might be restarted if ShouldRetry is true
/// Otherwise, the completed request is disposed and removed
/// Only the HTTP thread is allowed to use this list, no other thread may use it
/// </summary>
private readonly List<RCheevoHttpRequest> _activeHttpRequests = new();
private volatile bool _isActive;
private readonly Thread _httpThread;
/// <summary>
/// Base class for all HTTP requests to rcheevos servers
/// </summary>
public abstract class RCheevoHttpRequest : IDisposable
{
private readonly object _syncObject = new();
private readonly ManualResetEventSlim _completionEvent = new();
private bool _isDisposed;
public virtual bool ShouldRetry { get; protected set; }
public bool IsCompleted
{
get
{
lock (_syncObject)
{
return _isDisposed || _completionEvent.IsSet;
}
}
}
public abstract void DoRequest();
protected abstract void ResponseCallback(byte[] serv_resp);
public void Wait()
{
lock (_syncObject)
{
if (_isDisposed) return;
_completionEvent.Wait();
}
}
public virtual void Dispose()
{
if (_isDisposed) return;
lock (_syncObject)
{
_completionEvent.Wait();
_completionEvent.Dispose();
_isDisposed = true;
}
}
/// <summary>
/// Don't use, for FailedRCheevosRequest use only
/// </summary>
protected void DisposeWithoutWait()
{
#pragma warning disable BHI1101 // yeah, complain I guess, but this is a hack so meh
if (GetType() != typeof(FailedRCheevosRequest)) throw new InvalidOperationException();
#pragma warning restore BHI1101
_completionEvent.Dispose();
_isDisposed = true;
}
public void Reset()
{
ShouldRetry = false;
_completionEvent.Reset();
}
protected void InternalDoRequest(LibRCheevos.rc_error_t apiParamsResult, ref LibRCheevos.rc_api_request_t request)
{
if (apiParamsResult != LibRCheevos.rc_error_t.RC_OK)
{
// api params were bad, so we can't send a request
// therefore any retry will fail
ShouldRetry = false;
_completionEvent.Set();
_lib.rc_api_destroy_request(ref request);
return;
}
var apiTask = request.post_data != IntPtr.Zero
? HttpPost(request.URL, request.PostData)
: HttpGet(request.URL);
apiTask.ConfigureAwait(false);
_lib.rc_api_destroy_request(ref request);
var result = apiTask.Result; // FIXME: THIS IS BAD (but kind of needed?)
if (result is null) // likely a timeout
{
ShouldRetry = true;
_completionEvent.Set();
return;
}
ResponseCallback(result);
ShouldRetry = false; // this is a bit naive, but if the response callback "fails," retrying will just result in the same thing
_completionEvent.Set();
}
}
/// <summary>
/// Represents a generic failed rcheevos request
/// </summary>
public sealed class FailedRCheevosRequest : RCheevoHttpRequest
{
public static readonly FailedRCheevosRequest Singleton = new();
public override bool ShouldRetry => false;
protected override void ResponseCallback(byte[] serv_resp)
{
}
public override void DoRequest()
{
}
private FailedRCheevosRequest()
{
DisposeWithoutWait();
}
}
private void HttpRequestThreadProc()
{
while (_isActive)
{
if (_inactiveHttpRequests.TryPop(out var request))
{
Task.Run(request.DoRequest);
_activeHttpRequests.Add(request);
}
foreach (var activeRequest in _activeHttpRequests.Where(activeRequest => activeRequest.IsCompleted && activeRequest.ShouldRetry).ToArray())
{
activeRequest.Reset();
Task.Run(activeRequest.DoRequest);
}
_activeHttpRequests.RemoveAll(activeRequest =>
{
var shouldRemove = activeRequest.IsCompleted && !activeRequest.ShouldRetry;
if (shouldRemove)
{
activeRequest.Dispose();
}
return shouldRemove;
});
}
}
private static async Task<byte[]> HttpGet(string url)
{
var response = await _http.GetAsync(url).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
try
{
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
var response = await _http.GetAsync(url).ConfigureAwait(false);
return response.IsSuccessStatusCode
? await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false)
: null;
}
catch (Exception e)
{
Console.WriteLine(e);
return null;
}
return new byte[1];
}
private static async Task<byte[]> HttpPost(string url, string post)
@ -27,39 +203,15 @@ namespace BizHawk.Client.EmuHawk
{
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);
return response.IsSuccessStatusCode
? await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false)
: null;
}
catch (Exception e)
{
Console.WriteLine(e);
return new byte[1];
return null;
}
}
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,6 +1,5 @@
using System.Collections.Generic;
using System;
using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk
{
@ -10,39 +9,33 @@ namespace BizHawk.Client.EmuHawk
private readonly RCheevosLeaderboardListForm _lboardListForm = new();
#endif
private class LboardTriggerTask
private sealed class LboardTriggerRequest : RCheevoHttpRequest
{
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)
protected override void ResponseCallback(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;
if (res != LibRCheevos.rc_error_t.RC_OK)
{
Console.WriteLine($"LboardTriggerRequest failed in ResponseCallback with {res}");
}
}
public void DoRequest()
public override 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);
var apiParamsResult = _lib.rc_api_init_submit_lboard_entry_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
public LboardTriggerTask(string username, string api_token, int id, int value, string hash)
public LboardTriggerRequest(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; }
public class LBoard

View File

@ -9,15 +9,30 @@ namespace BizHawk.Client.EmuHawk
private event Action LoginStatusChanged;
private bool DoLogin(string username, string apiToken = null, string password = null)
private sealed class LoginRequest : RCheevoHttpRequest
{
Username = null;
ApiToken = null;
private LibRCheevos.rc_api_login_request_t _apiParams;
public string Username { get; private set; }
public string ApiToken { get; private set; }
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 =>
public override bool ShouldRetry => false;
public LoginRequest(string username, string apiToken = null, string password = null)
{
_apiParams = new(username, apiToken, password);
}
public override void DoRequest()
{
var apiParamsResult = _lib.rc_api_init_login_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
protected override void ResponseCallback(byte[] serv_resp)
{
Username = null;
ApiToken = null;
if (_lib.rc_api_process_login_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK)
{
Username = resp.Username;
@ -25,7 +40,17 @@ namespace BizHawk.Client.EmuHawk
}
_lib.rc_api_destroy_login_response(ref resp);
}).Wait(); // currently, this is done synchronously
}
}
private bool DoLogin(string username, string apiToken = null, string password = null)
{
var loginRequest = new LoginRequest(username, apiToken, password);
_inactiveHttpRequests.Push(loginRequest);
loginRequest.Wait();
Username = loginRequest.Username;
ApiToken = loginRequest.ApiToken;
return LoggedIn;
}

View File

@ -1,6 +1,5 @@
using System;
using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk
{
@ -8,31 +7,67 @@ namespace BizHawk.Client.EmuHawk
{
private bool RichPresenceActive { get; set; }
private string CurrentRichPresence { get; set; }
private bool GameSessionStartSuccessful { get; set; }
private Task _startGameSessionTask;
private sealed class StartGameSessionRequest : RCheevoHttpRequest
{
private LibRCheevos.rc_api_start_session_request_t _apiParams;
public StartGameSessionRequest(string username, string apiToken, int gameId)
{
_apiParams = new(username, apiToken, gameId);
}
public override void DoRequest()
{
var apiParamsResult = _lib.rc_api_init_start_session_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
protected override void ResponseCallback(byte[] serv_resp)
{
var res = _lib.rc_api_process_start_session_response(out var resp, serv_resp);
_lib.rc_api_destroy_start_session_response(ref resp);
if (res != LibRCheevos.rc_error_t.RC_OK)
{
Console.WriteLine($"StartGameSessionRequest failed in ResponseCallback with {res}");
}
}
}
private void StartGameSession()
{
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 =>
{
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);
});
_inactiveHttpRequests.Push(new StartGameSessionRequest(Username, ApiToken, _gameData.GameID));
}
private static void SendPing(string username, string api_token, int id, string rich_presence)
private sealed class PingRequest : RCheevoHttpRequest
{
var api_params = new LibRCheevos.rc_api_ping_request_t(username, api_token, id, rich_presence);
var res = _lib.rc_api_init_ping_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, static serv_resp =>
private LibRCheevos.rc_api_ping_request_t _apiParams;
public PingRequest(string username, string apiToken, int gameId, string richPresence)
{
_lib.rc_api_process_ping_response(out var resp, serv_resp);
_apiParams = new(username, apiToken, gameId, richPresence);
}
public override void DoRequest()
{
var apiParamsResult = _lib.rc_api_init_ping_request(out var api_req, ref _apiParams);
InternalDoRequest(apiParamsResult, ref api_req);
}
protected override void ResponseCallback(byte[] serv_resp)
{
var res = _lib.rc_api_process_ping_response(out var resp, serv_resp);
_lib.rc_api_destroy_ping_response(ref resp);
});
if (res != LibRCheevos.rc_error_t.RC_OK)
{
Console.WriteLine($"PingRequest failed in ResponseCallback with {res}");
}
}
}
private void SendPing()
{
_inactiveHttpRequests.Push(new PingRequest(Username, ApiToken, _gameData.GameID, CurrentRichPresence));
}
private readonly byte[] _richPresenceBuffer = new byte[1024];
@ -54,7 +89,7 @@ namespace BizHawk.Client.EmuHawk
var now = DateTime.Now;
if (now - _lastPingTime < _pingCooldown) return;
SendPing(Username, ApiToken, _gameData.GameID, CurrentRichPresence);
SendPing();
_lastPingTime = now;
}
}

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using BizHawk.BizInvoke;
@ -195,6 +194,10 @@ namespace BizHawk.Client.EmuHawk
Func<Config> getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback)
: base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback)
{
_isActive = true;
_httpThread = new(HttpRequestThreadProc) { IsBackground = true };
_httpThread.Start();
_runtime = default;
_lib.rc_runtime_init(ref _runtime);
Login();
@ -215,8 +218,21 @@ namespace BizHawk.Client.EmuHawk
public override void Dispose()
{
Task.WaitAll(_queuedCheevoUnlockTasks.Select(t => t.Task).ToArray());
Task.WaitAll(_queuedLboardTriggerTasks.Select(t => t.Task).ToArray());
while (!_inactiveHttpRequests.IsEmpty)
{
// wait until all pending http requests are enqueued
}
_isActive = false;
_httpThread.Join();
// the http thread is dead, so we can safely use _activeHttpRequests
foreach (var request in _activeHttpRequests)
{
if (request is ImageRequest) continue; // THIS IS BAD, I KNOW
request.Dispose(); // implicitly waits for the request to finish or timeout
}
_lib.rc_runtime_destroy(ref _runtime);
Stop();
_gameInfoForm.Dispose();
@ -234,7 +250,7 @@ namespace BizHawk.Client.EmuHawk
return;
}
_activeModeUnlocksTask.Wait();
_activeModeUnlocksRequest.Wait();
var size = _lib.rc_runtime_progress_size(ref _runtime, IntPtr.Zero);
if (size > 0)
{
@ -257,7 +273,7 @@ namespace BizHawk.Client.EmuHawk
HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode.");
}
_activeModeUnlocksTask.Wait();
_activeModeUnlocksRequest.Wait();
_lib.rc_runtime_reset(ref _runtime);
if (!File.Exists(path + ".rap")) return;
@ -351,13 +367,18 @@ namespace BizHawk.Client.EmuHawk
AllGamesVerified = !ids.Contains(0);
var gameId = ids.Count > 0 ? ids[0] : 0;
_gameData = new();
if (gameId != 0)
{
_gameData = _cachedGameDatas.TryGetValue(gameId, out var cachedGameData)
? new(cachedGameData, () => AllowUnofficialCheevos)
: GetGameData(Username, ApiToken, gameId, () => AllowUnofficialCheevos);
: GetGameData(gameId);
}
// this check seems redundant, but it covers the case where GetGameData failed somehow
if (_gameData.GameID != 0)
{
StartGameSession();
_cachedGameDatas.Remove(gameId);
@ -367,14 +388,13 @@ namespace BizHawk.Client.EmuHawk
}
else
{
_gameData = new();
_activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask;
_activeModeUnlocksRequest = _inactiveModeUnlocksRequest = FailedRCheevosRequest.Singleton;
}
}
else
{
_gameData = new();
_activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask;
_activeModeUnlocksRequest = _inactiveModeUnlocksRequest = FailedRCheevosRequest.Singleton;
}
// validate addresses now that we have cheevos init
@ -401,33 +421,13 @@ 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();
}
if (_gameData.GameID != 0)
{
if (HardcoreMode)
{
CheckHardcoreModeConditions();
}
CheckPing();
}
}
@ -454,7 +454,7 @@ namespace BizHawk.Client.EmuHawk
if (cheevo.IsOfficial)
{
_queuedCheevoUnlockTasks.Add(new(Username, ApiToken, evt->id, HardcoreMode, _gameHash));
_inactiveHttpRequests.Push(new CheevoUnlockRequest(Username, ApiToken, evt->id, HardcoreMode, _gameHash));
}
}
@ -539,7 +539,7 @@ namespace BizHawk.Client.EmuHawk
var lboard = _gameData.GetLboardById(evt->id);
if (!lboard.Invalid)
{
_queuedLboardTriggerTasks.Add(new(Username, ApiToken, evt->id, evt->value, _gameHash));
_inactiveHttpRequests.Push(new LboardTriggerRequest(Username, ApiToken, evt->id, evt->value, _gameHash));
if (!lboard.Hidden)
{
@ -601,7 +601,7 @@ namespace BizHawk.Client.EmuHawk
public override void OnFrameAdvance()
{
if (!LoggedIn || !_activeModeUnlocksTask.IsCompleted)
if (!LoggedIn || !_activeModeUnlocksRequest.IsCompleted)
{
return;
}