flip_world.c 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #include <flip_world.h>
  2. #include <flip_storage/storage.h>
  3. char *fps_choices_str[] = {"30", "60", "120", "240"};
  4. uint8_t fps_index = 0;
  5. char *yes_or_no_choices[] = {"No", "Yes"};
  6. uint8_t screen_always_on_index = 1;
  7. uint8_t sound_on_index = 1;
  8. uint8_t vibration_on_index = 1;
  9. char *player_sprite_choices[] = {"naked", "sword", "axe", "bow"};
  10. uint8_t player_sprite_index = 1;
  11. char *vgm_levels[] = {"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
  12. uint8_t vgm_x_index = 2;
  13. uint8_t vgm_y_index = 2;
  14. uint8_t game_mode_index = 0;
  15. float atof_(const char *nptr) { return (float)strtod(nptr, NULL); }
  16. float atof_furi(const FuriString *nptr) { return atof_(furi_string_get_cstr(nptr)); }
  17. bool is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
  18. bool is_enough_heap(size_t heap_size, bool check_blocks)
  19. {
  20. const size_t min_heap = heap_size + 1024; // 1KB buffer
  21. const size_t min_free = memmgr_get_free_heap();
  22. if (min_free < min_heap)
  23. {
  24. FURI_LOG_E(TAG, "Not enough heap memory: There are %zu bytes free.", min_free);
  25. return false;
  26. }
  27. if (check_blocks)
  28. {
  29. const size_t max_free_block = memmgr_heap_get_max_free_block();
  30. if (max_free_block < min_heap)
  31. {
  32. FURI_LOG_E(TAG, "Not enough free blocks: %zu bytes", max_free_block);
  33. return false;
  34. }
  35. }
  36. return true;
  37. }
  38. static DateTime *flip_world_get_rtc_time()
  39. {
  40. DateTime *rtc_time = malloc(sizeof(DateTime));
  41. if (!rtc_time)
  42. {
  43. FURI_LOG_E(TAG, "Failed to allocate memory for DateTime");
  44. return NULL;
  45. }
  46. furi_hal_rtc_get_datetime(rtc_time);
  47. return rtc_time;
  48. }
  49. static DateTime *flip_world_json_to_datetime(const char *str)
  50. {
  51. DateTime *rtc_time = malloc(sizeof(DateTime));
  52. if (!rtc_time)
  53. {
  54. FURI_LOG_E(TAG, "Failed to allocate memory for DateTime");
  55. return NULL;
  56. }
  57. char *hour = get_json_value("hour", str);
  58. char *minute = get_json_value("minute", str);
  59. char *second = get_json_value("second", str);
  60. char *day = get_json_value("day", str);
  61. char *month = get_json_value("month", str);
  62. char *year = get_json_value("year", str);
  63. char *weekday = get_json_value("weekday", str);
  64. if (!hour || !minute || !second || !day || !month || !year || !weekday)
  65. {
  66. FURI_LOG_E(TAG, "Failed to parse JSON");
  67. free(rtc_time);
  68. return NULL;
  69. }
  70. rtc_time->hour = atoi(hour);
  71. rtc_time->minute = atoi(minute);
  72. rtc_time->second = atoi(second);
  73. rtc_time->day = atoi(day);
  74. rtc_time->month = atoi(month);
  75. rtc_time->year = atoi(year);
  76. rtc_time->weekday = atoi(weekday);
  77. free(hour);
  78. free(minute);
  79. free(second);
  80. free(day);
  81. free(month);
  82. free(year);
  83. free(weekday);
  84. return rtc_time;
  85. }
  86. static FuriString *flip_world_datetime_to_json(DateTime *rtc_time)
  87. {
  88. if (!rtc_time)
  89. {
  90. FURI_LOG_E(TAG, "rtc_time is NULL");
  91. return NULL;
  92. }
  93. FuriString *json = furi_string_alloc();
  94. if (!json)
  95. {
  96. FURI_LOG_E(TAG, "Failed to allocate memory for JSON");
  97. return NULL;
  98. }
  99. furi_string_printf(
  100. json,
  101. "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
  102. rtc_time->hour,
  103. rtc_time->minute,
  104. rtc_time->second,
  105. rtc_time->day,
  106. rtc_time->month,
  107. rtc_time->year,
  108. rtc_time->weekday);
  109. return json;
  110. }
  111. static bool flip_world_save_rtc_time(DateTime *rtc_time)
  112. {
  113. if (!rtc_time)
  114. {
  115. FURI_LOG_E(TAG, "rtc_time is NULL");
  116. return false;
  117. }
  118. FuriString *json = flip_world_datetime_to_json(rtc_time);
  119. if (!json)
  120. {
  121. FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
  122. return false;
  123. }
  124. save_char("last_checked", furi_string_get_cstr(json));
  125. furi_string_free(json);
  126. return true;
  127. }
  128. //
  129. // Returns true if time_current is one hour (or more) later than the stored last_updated time
  130. //
  131. static bool flip_world_is_update_time(DateTime *time_current)
  132. {
  133. if (!time_current)
  134. {
  135. FURI_LOG_E(TAG, "time_current is NULL");
  136. return false;
  137. }
  138. char last_updated_old[32];
  139. if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
  140. {
  141. FURI_LOG_E(TAG, "Failed to load last_updated");
  142. FuriString *json = flip_world_datetime_to_json(time_current);
  143. if (json)
  144. {
  145. save_char("last_updated", furi_string_get_cstr(json));
  146. furi_string_free(json);
  147. }
  148. return false;
  149. }
  150. DateTime *last_updated_time = flip_world_json_to_datetime(last_updated_old);
  151. if (!last_updated_time)
  152. {
  153. FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
  154. return false;
  155. }
  156. bool time_diff = false;
  157. // If the date is different assume more than one hour has passed.
  158. if (time_current->year != last_updated_time->year ||
  159. time_current->month != last_updated_time->month ||
  160. time_current->day != last_updated_time->day)
  161. {
  162. time_diff = true;
  163. }
  164. else
  165. {
  166. // For the same day, compute seconds from midnight.
  167. int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
  168. int seconds_last = last_updated_time->hour * 3600 + last_updated_time->minute * 60 + last_updated_time->second;
  169. if ((seconds_current - seconds_last) >= 3600)
  170. {
  171. time_diff = true;
  172. }
  173. }
  174. if (time_diff)
  175. {
  176. FuriString *json = flip_world_datetime_to_json(time_current);
  177. if (json)
  178. {
  179. save_char("last_updated", furi_string_get_cstr(json));
  180. furi_string_free(json);
  181. }
  182. }
  183. free(last_updated_time);
  184. return time_diff;
  185. }
  186. // Sends a request to fetch the last updated date of the app.
  187. static bool flip_world_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
  188. {
  189. if (!fhttp)
  190. {
  191. FURI_LOG_E(TAG, "fhttp is NULL");
  192. return false;
  193. }
  194. return flipper_http_request(
  195. fhttp,
  196. GET,
  197. !flipper_server ? "https://www.jblanked.com/flipper/api/app/last-updated/flip_world/"
  198. : "//--TODO--//",
  199. "{\"Content-Type\":\"application/json\"}",
  200. NULL);
  201. }
  202. // Parses the server response and returns true if an update is available.
  203. static bool flip_world_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current)
  204. {
  205. if (!fhttp)
  206. {
  207. FURI_LOG_E(TAG, "fhttp is NULL");
  208. return false;
  209. }
  210. if (fhttp->state == ISSUE)
  211. {
  212. FURI_LOG_E(TAG, "Failed to fetch last app update");
  213. return false;
  214. }
  215. if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
  216. {
  217. FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
  218. return false;
  219. }
  220. // Save the server app version.
  221. save_char("server_app_version", fhttp->last_response);
  222. // Only check for an update if an hour or more has passed.
  223. if (flip_world_is_update_time(time_current))
  224. {
  225. char app_version[32];
  226. if (!load_char("app_version", app_version, sizeof(app_version)))
  227. {
  228. FURI_LOG_E(TAG, "Failed to load app version");
  229. return false;
  230. }
  231. if (strcmp(fhttp->last_response, app_version) != 0)
  232. {
  233. FURI_LOG_I(TAG, "Update available");
  234. return true;
  235. }
  236. else
  237. {
  238. FURI_LOG_I(TAG, "No update available");
  239. return false;
  240. }
  241. }
  242. return false; // Not yet time to update.
  243. }
  244. static bool flip_world_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
  245. {
  246. if (!fhttp)
  247. {
  248. FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
  249. return false;
  250. }
  251. char url[256];
  252. fhttp->save_received_data = false;
  253. fhttp->is_bytes_request = true;
  254. #ifndef FW_ORIGIN_Momentum
  255. snprintf(
  256. fhttp->file_path,
  257. sizeof(fhttp->file_path),
  258. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/flip_world.fap");
  259. #else
  260. snprintf(
  261. fhttp->file_path,
  262. sizeof(fhttp->file_path),
  263. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/FlipperHTTP/flip_world.fap");
  264. #endif
  265. if (flipper_server)
  266. {
  267. char build_id[32];
  268. snprintf(build_id, sizeof(build_id), "%s", "67f22eb325a4a6f1fb4a2c5d");
  269. uint8_t target;
  270. target = furi_hal_version_get_hw_target();
  271. uint16_t api_major, api_minor;
  272. furi_hal_info_get_api_version(&api_major, &api_minor);
  273. snprintf(
  274. url,
  275. sizeof(url),
  276. "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
  277. build_id,
  278. target,
  279. api_major,
  280. api_minor);
  281. }
  282. else
  283. {
  284. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/flip_world");
  285. }
  286. return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
  287. }
  288. // Updates the app. Uses the supplied current time for validating if update check should proceed.
  289. static void flip_world_update_app(FlipperHTTP *fhttp, DateTime *time_current)
  290. {
  291. if (!fhttp)
  292. {
  293. FURI_LOG_E(TAG, "fhttp is NULL");
  294. return;
  295. }
  296. if (!flip_world_last_app_update(fhttp, false))
  297. {
  298. FURI_LOG_E(TAG, "Failed to fetch last app update");
  299. return;
  300. }
  301. while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
  302. {
  303. // Wait for the request to finish.
  304. furi_delay_ms(100);
  305. }
  306. furi_timer_stop(fhttp->get_timeout_timer);
  307. if (flip_world_parse_last_app_update(fhttp, time_current))
  308. {
  309. easy_flipper_dialog("App Update", "FlipWorld update is available.\nPress BACK to install.");
  310. if (!flip_world_get_fap_file(fhttp, false))
  311. {
  312. FURI_LOG_E(TAG, "Failed to fetch fap file");
  313. return;
  314. }
  315. while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
  316. {
  317. furi_delay_ms(100);
  318. }
  319. furi_timer_stop(fhttp->get_timeout_timer);
  320. if (fhttp->state == ISSUE)
  321. {
  322. FURI_LOG_E(TAG, "Failed to fetch fap file");
  323. return;
  324. }
  325. easy_flipper_dialog("Update Done", "FlipWorld update is done.\nPress BACK to restart.");
  326. }
  327. else
  328. {
  329. FURI_LOG_I(TAG, "No update available");
  330. }
  331. }
  332. // Handles the app update routine. This function obtains the current RTC time,
  333. // checks the "last_checked" value, and if it is more than one hour old, calls for an update.
  334. bool flip_world_handle_app_update(FlipperHTTP *fhttp)
  335. {
  336. if (!fhttp)
  337. {
  338. FURI_LOG_E(TAG, "fhttp is NULL");
  339. return false;
  340. }
  341. DateTime *rtc_time = flip_world_get_rtc_time();
  342. if (!rtc_time)
  343. {
  344. FURI_LOG_E(TAG, "Failed to get RTC time");
  345. return false;
  346. }
  347. char last_checked[32];
  348. if (!load_char("last_checked", last_checked, sizeof(last_checked)))
  349. {
  350. // First time – save the current time and check for an update.
  351. flip_world_save_rtc_time(rtc_time);
  352. flip_world_update_app(fhttp, rtc_time);
  353. free(rtc_time);
  354. return true;
  355. }
  356. else
  357. {
  358. // Check if the current RTC time is at least one hour past the stored time.
  359. if (flip_world_is_update_time(rtc_time))
  360. {
  361. flip_world_update_app(fhttp, rtc_time);
  362. free(rtc_time);
  363. return true;
  364. }
  365. free(rtc_time);
  366. return false; // No update necessary.
  367. }
  368. }