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:
parent
e065263ff2
commit
b517228475
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue