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.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk namespace BizHawk.Client.EmuHawk
{ {
@ -10,37 +9,32 @@ namespace BizHawk.Client.EmuHawk
{ {
private readonly RCheevosAchievementListForm _cheevoListForm = new(); private readonly RCheevosAchievementListForm _cheevoListForm = new();
private class CheevoUnlockTask private sealed class CheevoUnlockRequest : RCheevoHttpRequest
{ {
private LibRCheevos.rc_api_award_achievement_request_t _apiParams; 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); var res = _lib.rc_api_process_award_achievement_response(out var resp, serv_resp);
_lib.rc_api_destroy_award_achievement_response(ref 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); var apiParamsResult = _lib.rc_api_init_award_achievement_request(out var api_req, ref _apiParams);
Task = SendAPIRequestIfOK(res, ref api_req, CheevoUnlockTaskCallback); 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); _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 CheevosActive { get; set; }
private bool AllowUnofficialCheevos { get; set; } private bool AllowUnofficialCheevos { get; set; }
@ -54,8 +48,11 @@ namespace BizHawk.Client.EmuHawk
public string Definition { get; } public string Definition { get; }
public string Author { get; } public string Author { get; }
private string BadgeName { get; } private string BadgeName { get; }
public Bitmap BadgeUnlocked { get; private set; } public Bitmap BadgeUnlocked => _badgeUnlockedRequest?.Image;
public Bitmap BadgeLocked { get; private set; } public Bitmap BadgeLocked => _badgeLockedRequest?.Image;
private ImageRequest _badgeUnlockedRequest, _badgeLockedRequest;
public DateTime Created { get; } public DateTime Created { get; }
public DateTime Updated { get; } public DateTime Updated { get; }
@ -83,10 +80,12 @@ namespace BizHawk.Client.EmuHawk
public bool IsEnabled => !Invalid && (IsOfficial || AllowUnofficialCheevos()); public bool IsEnabled => !Invalid && (IsOfficial || AllowUnofficialCheevos());
public bool IsOfficial => Category is LibRCheevos.rc_runtime_achievement_category_t.RC_ACHIEVEMENT_CATEGORY_CORE; 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); _badgeUnlockedRequest = new(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT);
GetImage(BadgeName, LibRCheevos.rc_api_image_type_t.RC_IMAGE_TYPE_ACHIEVEMENT_LOCKED, badge => BadgeLocked = badge); _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) public Cheevo(in LibRCheevos.rc_api_achievement_definition_t cheevo, Func<bool> allowUnofficialCheevos)
@ -99,8 +98,6 @@ namespace BizHawk.Client.EmuHawk
Definition = cheevo.Definition; Definition = cheevo.Definition;
Author = cheevo.Author; Author = cheevo.Author;
BadgeName = cheevo.BadgeName; BadgeName = cheevo.BadgeName;
BadgeUnlocked = null;
BadgeLocked = null;
Created = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.created).ToLocalTime(); 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(); Updated = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddSeconds(cheevo.updated).ToLocalTime();
IsSoftcoreUnlocked = false; IsSoftcoreUnlocked = false;
@ -120,8 +117,6 @@ namespace BizHawk.Client.EmuHawk
Definition = cheevo.Definition; Definition = cheevo.Definition;
Author = cheevo.Author; Author = cheevo.Author;
BadgeName = cheevo.BadgeName; BadgeName = cheevo.BadgeName;
BadgeUnlocked = null;
BadgeLocked = null;
Created = cheevo.Created; Created = cheevo.Created;
Updated = cheevo.Updated; Updated = cheevo.Updated;
IsSoftcoreUnlocked = false; IsSoftcoreUnlocked = false;
@ -148,7 +143,7 @@ namespace BizHawk.Client.EmuHawk
return; return;
} }
_activeModeUnlocksTask.Wait(); _activeModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable) foreach (var cheevo in _gameData.CheevoEnumerable)
{ {
@ -173,7 +168,7 @@ namespace BizHawk.Client.EmuHawk
{ {
if (_gameData == null || _gameData.GameID == 0) return; if (_gameData == null || _gameData.GameID == 0) return;
_inactiveModeUnlocksTask.Wait(); _inactiveModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable) foreach (var cheevo in _gameData.CheevoEnumerable)
{ {
@ -183,7 +178,7 @@ namespace BizHawk.Client.EmuHawk
} }
} }
_activeModeUnlocksTask.Wait(); _activeModeUnlocksRequest.Wait();
foreach (var cheevo in _gameData.CheevoEnumerable) foreach (var cheevo in _gameData.CheevoEnumerable)
{ {

View File

@ -4,7 +4,6 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using BizHawk.Common.CollectionExtensions; using BizHawk.Common.CollectionExtensions;
@ -22,7 +21,131 @@ namespace BizHawk.Client.EmuHawk
private GameData _gameData; private GameData _gameData;
private readonly Dictionary<int, GameData> _cachedGameDatas = new(); // keep game data around to avoid unneeded API calls for a simple RebootCore 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 public class GameData
{ {
@ -30,9 +153,11 @@ namespace BizHawk.Client.EmuHawk
public ConsoleID ConsoleID { get; } public ConsoleID ConsoleID { get; }
public string Title { get; } public string Title { get; }
private string ImageName { get; } private string ImageName { get; }
public Bitmap GameBadge { get; private set; } public Bitmap GameBadge => _gameBadgeImageRequest?.Image;
public string RichPresenseScript { get; } public string RichPresenseScript { get; }
private ImageRequest _gameBadgeImageRequest;
private readonly IReadOnlyDictionary<int, Cheevo> _cheevos; private readonly IReadOnlyDictionary<int, Cheevo> _cheevos;
private readonly IReadOnlyDictionary<int, LBoard> _lboards; private readonly IReadOnlyDictionary<int, LBoard> _lboards;
@ -42,43 +167,26 @@ namespace BizHawk.Client.EmuHawk
public Cheevo GetCheevoById(int i) => _cheevos[i]; public Cheevo GetCheevoById(int i) => _cheevos[i];
public LBoard GetLboardById(int i) => _lboards[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); return new(username, api_token, GameID, hardcore, _cheevos, callback);
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();
});
} }
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) foreach (var cheevo in _cheevos.Values)
{ {
cheevo.LoadImages(); cheevo.LoadImages(requests);
} }
return requests;
} }
public int TotalCheevoPoints(bool hardcore) public int TotalCheevoPoints(bool hardcore)
@ -90,7 +198,6 @@ namespace BizHawk.Client.EmuHawk
ConsoleID = resp.console_id; ConsoleID = resp.console_id;
Title = resp.Title; Title = resp.Title;
ImageName = resp.ImageName; ImageName = resp.ImageName;
GameBadge = null;
RichPresenseScript = resp.RichPresenceScript; RichPresenseScript = resp.RichPresenceScript;
var cheevos = new Dictionary<int, Cheevo>(); var cheevos = new Dictionary<int, Cheevo>();
@ -118,7 +225,6 @@ namespace BizHawk.Client.EmuHawk
ConsoleID = gameData.ConsoleID; ConsoleID = gameData.ConsoleID;
Title = gameData.Title; Title = gameData.Title;
ImageName = gameData.ImageName; ImageName = gameData.ImageName;
GameBadge = null;
RichPresenseScript = gameData.RichPresenseScript; RichPresenseScript = gameData.RichPresenseScript;
_cheevos = gameData.CheevoEnumerable.ToDictionary<Cheevo, int, Cheevo>(cheevo => cheevo.ID, cheevo => new(in cheevo, allowUnofficialCheevos)); _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); private LibRCheevos.rc_api_resolve_hash_request_t _apiParams;
var ret = 0; public int GameID { get; private set; }
var res = _lib.rc_api_init_resolve_hash_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, serv_resp => // 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); _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) protected override int IdentifyHash(string hash)
@ -169,7 +301,7 @@ namespace BizHawk.Client.EmuHawk
private void InitGameData() private void InitGameData()
{ {
_activeModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, HardcoreMode, () => _activeModeUnlocksRequest = _gameData.InitUnlocks(Username, ApiToken, HardcoreMode, () =>
{ {
foreach (var cheevo in _gameData.CheevoEnumerable) foreach (var cheevo in _gameData.CheevoEnumerable)
{ {
@ -179,9 +311,13 @@ namespace BizHawk.Client.EmuHawk
} }
} }
}); });
_inactiveHttpRequests.Push(_activeModeUnlocksRequest);
_inactiveModeUnlocksTask = _gameData.InitUnlocks(Username, ApiToken, !HardcoreMode); _inactiveModeUnlocksRequest = _gameData.InitUnlocks(Username, ApiToken, !HardcoreMode);
_gameData.LoadImages(); _inactiveHttpRequests.Push(_inactiveModeUnlocksRequest);
var loadImageRequests = _gameData.LoadImages();
_inactiveHttpRequests.PushRange(loadImageRequests.ToArray());
foreach (var lboard in _gameData.LBoardEnumerable) 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 gameDataRequest = new GameDataRequest(Username, ApiToken, id, () => AllowUnofficialCheevos);
var ret = new GameData(); _inactiveHttpRequests.Push(gameDataRequest);
var res = _lib.rc_api_init_fetch_game_data_request(out var api_req, ref api_params); gameDataRequest.Wait();
SendAPIRequestIfOK(res, ref api_req, serv_resp => return gameDataRequest.GameData;
{
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);
});
} }
} }
} }

View File

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -11,14 +13,188 @@ namespace BizHawk.Client.EmuHawk
{ {
private static readonly HttpClient _http = new() { DefaultRequestHeaders = { ConnectionClose = true } }; 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) private static async Task<byte[]> HttpGet(string url)
{ {
var response = await _http.GetAsync(url).ConfigureAwait(false); try
if (response.IsSuccessStatusCode)
{ {
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) 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 content = new StringContent(post, Encoding.UTF8, "application/x-www-form-urlencoded");
using var response = await _http.PostAsync(url, content).ConfigureAwait(false); using var response = await _http.PostAsync(url, content).ConfigureAwait(false);
if (!response.IsSuccessStatusCode) return response.IsSuccessStatusCode
{ ? await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false)
return new byte[1]; : null;
}
return await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(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.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk namespace BizHawk.Client.EmuHawk
{ {
@ -10,39 +9,33 @@ namespace BizHawk.Client.EmuHawk
private readonly RCheevosLeaderboardListForm _lboardListForm = new(); private readonly RCheevosLeaderboardListForm _lboardListForm = new();
#endif #endif
private class LboardTriggerTask private sealed class LboardTriggerRequest : RCheevoHttpRequest
{ {
private LibRCheevos.rc_api_submit_lboard_entry_request_t _apiParams; 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); 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); _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); var apiParamsResult = _lib.rc_api_init_submit_lboard_entry_request(out var api_req, ref _apiParams);
Task = SendAPIRequestIfOK(res, ref api_req, LboardTriggerTaskCallback); 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); _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 bool LBoardsActive { get; set; }
private LBoard CurrentLboard { get; set; } private LBoard CurrentLboard { get; set; }
public class LBoard public class LBoard

View File

@ -9,15 +9,30 @@ namespace BizHawk.Client.EmuHawk
private event Action LoginStatusChanged; private event Action LoginStatusChanged;
private bool DoLogin(string username, string apiToken = null, string password = null) private sealed class LoginRequest : RCheevoHttpRequest
{ {
Username = null; private LibRCheevos.rc_api_login_request_t _apiParams;
ApiToken = null; 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); public override bool ShouldRetry => false;
var res = _lib.rc_api_init_login_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, serv_resp => 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) if (_lib.rc_api_process_login_response(out var resp, serv_resp) == LibRCheevos.rc_error_t.RC_OK)
{ {
Username = resp.Username; Username = resp.Username;
@ -25,7 +40,17 @@ namespace BizHawk.Client.EmuHawk
} }
_lib.rc_api_destroy_login_response(ref resp); _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; return LoggedIn;
} }

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Text; using System.Text;
using System.Threading.Tasks;
namespace BizHawk.Client.EmuHawk namespace BizHawk.Client.EmuHawk
{ {
@ -8,31 +7,67 @@ namespace BizHawk.Client.EmuHawk
{ {
private bool RichPresenceActive { get; set; } private bool RichPresenceActive { get; set; }
private string CurrentRichPresence { 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() private void StartGameSession()
{ {
GameSessionStartSuccessful = false; _inactiveHttpRequests.Push(new StartGameSessionRequest(Username, ApiToken, _gameData.GameID));
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);
});
} }
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); private LibRCheevos.rc_api_ping_request_t _apiParams;
var res = _lib.rc_api_init_ping_request(out var api_req, ref api_params);
SendAPIRequestIfOK(res, ref api_req, static serv_resp => 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); _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]; private readonly byte[] _richPresenceBuffer = new byte[1024];
@ -54,7 +89,7 @@ namespace BizHawk.Client.EmuHawk
var now = DateTime.Now; var now = DateTime.Now;
if (now - _lastPingTime < _pingCooldown) return; if (now - _lastPingTime < _pingCooldown) return;
SendPing(Username, ApiToken, _gameData.GameID, CurrentRichPresence); SendPing();
_lastPingTime = now; _lastPingTime = now;
} }
} }

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using BizHawk.BizInvoke; using BizHawk.BizInvoke;
@ -195,6 +194,10 @@ namespace BizHawk.Client.EmuHawk
Func<Config> getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback) Func<Config> getConfig, ToolStripItemCollection raDropDownItems, Action shutdownRACallback)
: base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback) : base(mainForm, inputManager, tools, getConfig, raDropDownItems, shutdownRACallback)
{ {
_isActive = true;
_httpThread = new(HttpRequestThreadProc) { IsBackground = true };
_httpThread.Start();
_runtime = default; _runtime = default;
_lib.rc_runtime_init(ref _runtime); _lib.rc_runtime_init(ref _runtime);
Login(); Login();
@ -215,8 +218,21 @@ namespace BizHawk.Client.EmuHawk
public override void Dispose() public override void Dispose()
{ {
Task.WaitAll(_queuedCheevoUnlockTasks.Select(t => t.Task).ToArray()); while (!_inactiveHttpRequests.IsEmpty)
Task.WaitAll(_queuedLboardTriggerTasks.Select(t => t.Task).ToArray()); {
// 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); _lib.rc_runtime_destroy(ref _runtime);
Stop(); Stop();
_gameInfoForm.Dispose(); _gameInfoForm.Dispose();
@ -234,7 +250,7 @@ namespace BizHawk.Client.EmuHawk
return; return;
} }
_activeModeUnlocksTask.Wait(); _activeModeUnlocksRequest.Wait();
var size = _lib.rc_runtime_progress_size(ref _runtime, IntPtr.Zero); var size = _lib.rc_runtime_progress_size(ref _runtime, IntPtr.Zero);
if (size > 0) if (size > 0)
{ {
@ -257,7 +273,7 @@ namespace BizHawk.Client.EmuHawk
HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode."); HandleHardcoreModeDisable("Loading savestates is not allowed in hardcore mode.");
} }
_activeModeUnlocksTask.Wait(); _activeModeUnlocksRequest.Wait();
_lib.rc_runtime_reset(ref _runtime); _lib.rc_runtime_reset(ref _runtime);
if (!File.Exists(path + ".rap")) return; if (!File.Exists(path + ".rap")) return;
@ -351,13 +367,18 @@ namespace BizHawk.Client.EmuHawk
AllGamesVerified = !ids.Contains(0); AllGamesVerified = !ids.Contains(0);
var gameId = ids.Count > 0 ? ids[0] : 0; var gameId = ids.Count > 0 ? ids[0] : 0;
_gameData = new();
if (gameId != 0) if (gameId != 0)
{ {
_gameData = _cachedGameDatas.TryGetValue(gameId, out var cachedGameData) _gameData = _cachedGameDatas.TryGetValue(gameId, out var cachedGameData)
? new(cachedGameData, () => AllowUnofficialCheevos) ? 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(); StartGameSession();
_cachedGameDatas.Remove(gameId); _cachedGameDatas.Remove(gameId);
@ -367,14 +388,13 @@ namespace BizHawk.Client.EmuHawk
} }
else else
{ {
_gameData = new(); _activeModeUnlocksRequest = _inactiveModeUnlocksRequest = FailedRCheevosRequest.Singleton;
_activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask;
} }
} }
else else
{ {
_gameData = new(); _gameData = new();
_activeModeUnlocksTask = _inactiveModeUnlocksTask = Task.CompletedTask; _activeModeUnlocksRequest = _inactiveModeUnlocksRequest = FailedRCheevosRequest.Singleton;
} }
// validate addresses now that we have cheevos init // validate addresses now that we have cheevos init
@ -401,33 +421,13 @@ namespace BizHawk.Client.EmuHawk
return; 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 (_gameData.GameID != 0)
{ {
if (HardcoreMode)
{
CheckHardcoreModeConditions();
}
CheckPing(); CheckPing();
} }
} }
@ -454,7 +454,7 @@ namespace BizHawk.Client.EmuHawk
if (cheevo.IsOfficial) 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); var lboard = _gameData.GetLboardById(evt->id);
if (!lboard.Invalid) 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) if (!lboard.Hidden)
{ {
@ -601,7 +601,7 @@ namespace BizHawk.Client.EmuHawk
public override void OnFrameAdvance() public override void OnFrameAdvance()
{ {
if (!LoggedIn || !_activeModeUnlocksTask.IsCompleted) if (!LoggedIn || !_activeModeUnlocksRequest.IsCompleted)
{ {
return; return;
} }