AmiiboWindow.cs 14 KB

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