AmiiboWindow.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. using Gtk;
  2. using Ryujinx.Common;
  3. using Ryujinx.Common.Configuration;
  4. using Ryujinx.Ui.Widgets;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Net.Http;
  10. using System.Reflection;
  11. using System.Text;
  12. using System.Text.Json;
  13. using System.Text.Json.Serialization;
  14. using System.Threading.Tasks;
  15. namespace Ryujinx.Ui.Windows
  16. {
  17. public partial class AmiiboWindow : Window
  18. {
  19. private struct AmiiboJson
  20. {
  21. [JsonPropertyName("amiibo")]
  22. public List<AmiiboApi> Amiibo { get; set; }
  23. [JsonPropertyName("lastUpdated")]
  24. public DateTime LastUpdated { get; set; }
  25. }
  26. private struct AmiiboApi
  27. {
  28. [JsonPropertyName("name")]
  29. public string Name { get; set; }
  30. [JsonPropertyName("head")]
  31. public string Head { get; set; }
  32. [JsonPropertyName("tail")]
  33. public string Tail { get; set; }
  34. [JsonPropertyName("image")]
  35. public string Image { get; set; }
  36. [JsonPropertyName("amiiboSeries")]
  37. public string AmiiboSeries { get; set; }
  38. [JsonPropertyName("character")]
  39. public string Character { get; set; }
  40. [JsonPropertyName("gameSeries")]
  41. public string GameSeries { get; set; }
  42. [JsonPropertyName("type")]
  43. public string Type { get; set; }
  44. [JsonPropertyName("release")]
  45. public Dictionary<string, string> Release { get; set; }
  46. [JsonPropertyName("gamesSwitch")]
  47. public List<AmiiboApiGamesSwitch> GamesSwitch { get; set; }
  48. }
  49. private class AmiiboApiGamesSwitch
  50. {
  51. [JsonPropertyName("amiiboUsage")]
  52. public List<AmiiboApiUsage> AmiiboUsage { get; set; }
  53. [JsonPropertyName("gameID")]
  54. public List<string> GameId { get; set; }
  55. [JsonPropertyName("gameName")]
  56. public string GameName { get; set; }
  57. }
  58. private class AmiiboApiUsage
  59. {
  60. [JsonPropertyName("Usage")]
  61. public string Usage { get; set; }
  62. [JsonPropertyName("write")]
  63. public bool Write { get; set; }
  64. }
  65. private const string DEFAULT_JSON = "{ \"amiibo\": [] }";
  66. public string AmiiboId { get; private set; }
  67. public int DeviceId { get; set; }
  68. public string TitleId { get; set; }
  69. public string LastScannedAmiiboId { get; set; }
  70. public bool LastScannedAmiiboShowAll { get; set; }
  71. public ResponseType Response { get; private set; }
  72. public bool UseRandomUuid
  73. {
  74. get
  75. {
  76. return _randomUuidCheckBox.Active;
  77. }
  78. }
  79. private readonly HttpClient _httpClient;
  80. private readonly string _amiiboJsonPath;
  81. private readonly byte[] _amiiboLogoBytes;
  82. private List<AmiiboApi> _amiiboList;
  83. public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo")
  84. {
  85. Icon = new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png");
  86. InitializeComponent();
  87. _httpClient = new HttpClient()
  88. {
  89. Timeout = TimeSpan.FromMilliseconds(5000)
  90. };
  91. Directory.CreateDirectory(System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
  92. _amiiboJsonPath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
  93. _amiiboList = new List<AmiiboApi>();
  94. _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx/Ui/Resources/Logo_Amiibo.png");
  95. _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
  96. _scanButton.Sensitive = false;
  97. _randomUuidCheckBox.Sensitive = false;
  98. _ = LoadContentAsync();
  99. }
  100. private async Task LoadContentAsync()
  101. {
  102. string amiiboJsonString = DEFAULT_JSON;
  103. if (File.Exists(_amiiboJsonPath))
  104. {
  105. amiiboJsonString = File.ReadAllText(_amiiboJsonPath);
  106. if (await NeedsUpdate(JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).LastUpdated))
  107. {
  108. amiiboJsonString = await DownloadAmiiboJson();
  109. }
  110. }
  111. else
  112. {
  113. try
  114. {
  115. amiiboJsonString = await DownloadAmiiboJson();
  116. }
  117. catch
  118. {
  119. ShowInfoDialog();
  120. Close();
  121. }
  122. }
  123. _amiiboList = JsonSerializer.Deserialize<AmiiboJson>(amiiboJsonString).Amiibo;
  124. _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
  125. if (LastScannedAmiiboShowAll)
  126. {
  127. _showAllCheckBox.Click();
  128. }
  129. ParseAmiiboData();
  130. _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked;
  131. }
  132. private void ParseAmiiboData()
  133. {
  134. List<string> comboxItemList = new List<string>();
  135. for (int i = 0; i < _amiiboList.Count; i++)
  136. {
  137. if (!comboxItemList.Contains(_amiiboList[i].AmiiboSeries))
  138. {
  139. if (!_showAllCheckBox.Active)
  140. {
  141. foreach (var game in _amiiboList[i].GamesSwitch)
  142. {
  143. if (game != null)
  144. {
  145. if (game.GameId.Contains(TitleId))
  146. {
  147. comboxItemList.Add(_amiiboList[i].AmiiboSeries);
  148. _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
  149. break;
  150. }
  151. }
  152. }
  153. }
  154. else
  155. {
  156. comboxItemList.Add(_amiiboList[i].AmiiboSeries);
  157. _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
  158. }
  159. }
  160. }
  161. _amiiboSeriesComboBox.Changed += SeriesComboBox_Changed;
  162. _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
  163. if (LastScannedAmiiboId != "")
  164. {
  165. SelectLastScannedAmiibo();
  166. }
  167. else
  168. {
  169. _amiiboSeriesComboBox.Active = 0;
  170. }
  171. }
  172. private void SelectLastScannedAmiibo()
  173. {
  174. bool isSet = _amiiboSeriesComboBox.SetActiveId(_amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == LastScannedAmiiboId).AmiiboSeries);
  175. isSet = _amiiboCharsComboBox.SetActiveId(LastScannedAmiiboId);
  176. if (isSet == false)
  177. {
  178. _amiiboSeriesComboBox.Active = 0;
  179. }
  180. }
  181. private async Task<bool> NeedsUpdate(DateTime oldLastModified)
  182. {
  183. try
  184. {
  185. HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
  186. if (response.IsSuccessStatusCode)
  187. {
  188. return response.Content.Headers.LastModified != oldLastModified;
  189. }
  190. return false;
  191. }
  192. catch
  193. {
  194. ShowInfoDialog();
  195. return false;
  196. }
  197. }
  198. private async Task<string> DownloadAmiiboJson()
  199. {
  200. HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
  201. if (response.IsSuccessStatusCode)
  202. {
  203. string amiiboJsonString = await response.Content.ReadAsStringAsync();
  204. using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
  205. {
  206. dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
  207. }
  208. return amiiboJsonString;
  209. }
  210. else
  211. {
  212. GtkDialog.CreateInfoDialog($"Amiibo API", "An error occured while fetching information from the API.");
  213. Close();
  214. }
  215. return DEFAULT_JSON;
  216. }
  217. private async Task UpdateAmiiboPreview(string imageUrl)
  218. {
  219. HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
  220. if (response.IsSuccessStatusCode)
  221. {
  222. byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
  223. Gdk.Pixbuf amiiboPreview = new Gdk.Pixbuf(amiiboPreviewBytes);
  224. float ratio = Math.Min((float)_amiiboImage.AllocatedWidth / amiiboPreview.Width,
  225. (float)_amiiboImage.AllocatedHeight / amiiboPreview.Height);
  226. int resizeHeight = (int)(amiiboPreview.Height * ratio);
  227. int resizeWidth = (int)(amiiboPreview.Width * ratio);
  228. _amiiboImage.Pixbuf = amiiboPreview.ScaleSimple(resizeWidth, resizeHeight, Gdk.InterpType.Bilinear);
  229. }
  230. }
  231. private void ShowInfoDialog()
  232. {
  233. GtkDialog.CreateInfoDialog($"Amiibo API", "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.");
  234. }
  235. //
  236. // Events
  237. //
  238. private void SeriesComboBox_Changed(object sender, EventArgs args)
  239. {
  240. _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
  241. _amiiboCharsComboBox.RemoveAll();
  242. List<AmiiboApi> amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList();
  243. List<string> comboxItemList = new List<string>();
  244. for (int i = 0; i < amiiboSortedList.Count; i++)
  245. {
  246. if (!comboxItemList.Contains(amiiboSortedList[i].Head + amiiboSortedList[i].Tail))
  247. {
  248. if (!_showAllCheckBox.Active)
  249. {
  250. foreach (var game in amiiboSortedList[i].GamesSwitch)
  251. {
  252. if (game != null)
  253. {
  254. if (game.GameId.Contains(TitleId))
  255. {
  256. comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
  257. _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
  258. break;
  259. }
  260. }
  261. }
  262. }
  263. else
  264. {
  265. comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
  266. _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
  267. }
  268. }
  269. }
  270. _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
  271. _amiiboCharsComboBox.Active = 0;
  272. _scanButton.Sensitive = true;
  273. _randomUuidCheckBox.Sensitive = true;
  274. }
  275. private void CharacterComboBox_Changed(object sender, EventArgs args)
  276. {
  277. AmiiboId = _amiiboCharsComboBox.ActiveId;
  278. _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
  279. string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == _amiiboCharsComboBox.ActiveId).Image;
  280. string usageString = "";
  281. for (int i = 0; i < _amiiboList.Count; i++)
  282. {
  283. if (_amiiboList[i].Head + _amiiboList[i].Tail == _amiiboCharsComboBox.ActiveId)
  284. {
  285. bool writable = false;
  286. foreach (var item in _amiiboList[i].GamesSwitch)
  287. {
  288. if (item.GameId.Contains(TitleId))
  289. {
  290. foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
  291. {
  292. usageString += Environment.NewLine + $"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}";
  293. writable = usageItem.Write;
  294. }
  295. }
  296. }
  297. if (usageString.Length == 0)
  298. {
  299. usageString = "Unknown.";
  300. }
  301. _gameUsageLabel.Text = $"Usage{(writable ? " (Writable)" : "")} : {usageString}";
  302. }
  303. }
  304. _ = UpdateAmiiboPreview(imageUrl);
  305. }
  306. private void ShowAllCheckBox_Clicked(object sender, EventArgs e)
  307. {
  308. _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
  309. _amiiboSeriesComboBox.Changed -= SeriesComboBox_Changed;
  310. _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
  311. _amiiboSeriesComboBox.RemoveAll();
  312. _amiiboCharsComboBox.RemoveAll();
  313. _scanButton.Sensitive = false;
  314. _randomUuidCheckBox.Sensitive = false;
  315. new Task(() => ParseAmiiboData()).Start();
  316. }
  317. private void ScanButton_Pressed(object sender, EventArgs args)
  318. {
  319. LastScannedAmiiboShowAll = _showAllCheckBox.Active;
  320. Response = ResponseType.Ok;
  321. Close();
  322. }
  323. private void CancelButton_Pressed(object sender, EventArgs args)
  324. {
  325. AmiiboId = "";
  326. LastScannedAmiiboId = "";
  327. LastScannedAmiiboShowAll = false;
  328. Response = ResponseType.Cancel;
  329. Close();
  330. }
  331. protected override void Dispose(bool disposing)
  332. {
  333. _httpClient.Dispose();
  334. base.Dispose(disposing);
  335. }
  336. }
  337. }