AmiiboWindow.cs 13 KB

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