flip_world.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  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 bool flip_world_json_to_datetime(DateTime *rtc_time, FuriString *str)
  39. {
  40. if (!rtc_time || !str)
  41. {
  42. FURI_LOG_E(TAG, "rtc_time or str is NULL");
  43. return false;
  44. }
  45. FuriString *hour = get_json_value_furi("hour", str);
  46. if (hour)
  47. {
  48. rtc_time->hour = atoi(furi_string_get_cstr(hour));
  49. furi_string_free(hour);
  50. }
  51. FuriString *minute = get_json_value_furi("minute", str);
  52. if (minute)
  53. {
  54. rtc_time->minute = atoi(furi_string_get_cstr(minute));
  55. furi_string_free(minute);
  56. }
  57. FuriString *second = get_json_value_furi("second", str);
  58. if (second)
  59. {
  60. rtc_time->second = atoi(furi_string_get_cstr(second));
  61. furi_string_free(second);
  62. }
  63. FuriString *day = get_json_value_furi("day", str);
  64. if (day)
  65. {
  66. rtc_time->day = atoi(furi_string_get_cstr(day));
  67. furi_string_free(day);
  68. }
  69. FuriString *month = get_json_value_furi("month", str);
  70. if (month)
  71. {
  72. rtc_time->month = atoi(furi_string_get_cstr(month));
  73. furi_string_free(month);
  74. }
  75. FuriString *year = get_json_value_furi("year", str);
  76. if (year)
  77. {
  78. rtc_time->year = atoi(furi_string_get_cstr(year));
  79. furi_string_free(year);
  80. }
  81. FuriString *weekday = get_json_value_furi("weekday", str);
  82. if (weekday)
  83. {
  84. rtc_time->weekday = atoi(furi_string_get_cstr(weekday));
  85. furi_string_free(weekday);
  86. }
  87. return datetime_validate_datetime(rtc_time);
  88. }
  89. static FuriString *flip_world_datetime_to_json(DateTime *rtc_time)
  90. {
  91. if (!rtc_time)
  92. {
  93. FURI_LOG_E(TAG, "rtc_time is NULL");
  94. return NULL;
  95. }
  96. char json[256];
  97. snprintf(
  98. json,
  99. sizeof(json),
  100. "{\"hour\":%d,\"minute\":%d,\"second\":%d,\"day\":%d,\"month\":%d,\"year\":%d,\"weekday\":%d}",
  101. rtc_time->hour,
  102. rtc_time->minute,
  103. rtc_time->second,
  104. rtc_time->day,
  105. rtc_time->month,
  106. rtc_time->year,
  107. rtc_time->weekday);
  108. return furi_string_alloc_set_str(json);
  109. }
  110. static bool flip_world_save_rtc_time(DateTime *rtc_time)
  111. {
  112. if (!rtc_time)
  113. {
  114. FURI_LOG_E(TAG, "rtc_time is NULL");
  115. return false;
  116. }
  117. FuriString *json = flip_world_datetime_to_json(rtc_time);
  118. if (!json)
  119. {
  120. FURI_LOG_E(TAG, "Failed to convert DateTime to JSON");
  121. return false;
  122. }
  123. save_char("last_checked", furi_string_get_cstr(json));
  124. furi_string_free(json);
  125. return true;
  126. }
  127. //
  128. // Returns true if time_current is one hour (or more) later than the stored last_updated time
  129. //
  130. static bool flip_world_is_update_time(DateTime *time_current)
  131. {
  132. if (!time_current)
  133. {
  134. FURI_LOG_E(TAG, "time_current is NULL");
  135. return false;
  136. }
  137. char last_updated_old[128];
  138. if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old)))
  139. {
  140. FURI_LOG_E(TAG, "Failed to load last_updated");
  141. FuriString *json = flip_world_datetime_to_json(time_current);
  142. if (json)
  143. {
  144. save_char("last_updated", furi_string_get_cstr(json));
  145. furi_string_free(json);
  146. }
  147. return false;
  148. }
  149. DateTime last_updated_time;
  150. FuriString *last_updated_furi = char_to_furi_string(last_updated_old);
  151. if (!last_updated_furi)
  152. {
  153. FURI_LOG_E(TAG, "Failed to convert char to FuriString");
  154. return false;
  155. }
  156. if (!flip_world_json_to_datetime(&last_updated_time, last_updated_furi))
  157. {
  158. FURI_LOG_E(TAG, "Failed to convert JSON to DateTime");
  159. furi_string_free(last_updated_furi);
  160. return false;
  161. }
  162. furi_string_free(last_updated_furi); // Free after usage.
  163. bool time_diff = false;
  164. // If the date is different assume more than one hour has passed.
  165. if (time_current->year != last_updated_time.year ||
  166. time_current->month != last_updated_time.month ||
  167. time_current->day != last_updated_time.day)
  168. {
  169. time_diff = true;
  170. }
  171. else
  172. {
  173. // For the same day, compute seconds from midnight.
  174. int seconds_current = time_current->hour * 3600 + time_current->minute * 60 + time_current->second;
  175. int seconds_last = last_updated_time.hour * 3600 + last_updated_time.minute * 60 + last_updated_time.second;
  176. if ((seconds_current - seconds_last) >= 3600)
  177. {
  178. time_diff = true;
  179. }
  180. }
  181. return time_diff;
  182. }
  183. // Sends a request to fetch the last updated date of the app.
  184. static bool flip_world_last_app_update(FlipperHTTP *fhttp, bool flipper_server)
  185. {
  186. if (!fhttp)
  187. {
  188. FURI_LOG_E(TAG, "fhttp is NULL");
  189. return false;
  190. }
  191. char url[256];
  192. if (flipper_server)
  193. {
  194. // make sure folder is created
  195. char directory_path[256];
  196. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
  197. // Create the directory
  198. Storage *storage = furi_record_open(RECORD_STORAGE);
  199. storage_common_mkdir(storage, directory_path);
  200. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
  201. storage_common_mkdir(storage, directory_path);
  202. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/last_update_request.txt");
  203. storage_simply_remove_recursive(storage, directory_path); // ensure the file is empty
  204. furi_record_close(RECORD_STORAGE);
  205. fhttp->save_received_data = true;
  206. fhttp->is_bytes_request = false;
  207. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/last_update_request.txt");
  208. snprintf(url, sizeof(url), "https://catalog.flipperzero.one/api/v0/0/application/%s?is_latest_release_version=true", BUILD_ID);
  209. return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
  210. }
  211. else
  212. {
  213. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/last-updated/flip_world/");
  214. return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
  215. }
  216. }
  217. // Parses the server response and returns true if an update is available.
  218. static bool flip_world_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
  219. {
  220. if (!fhttp)
  221. {
  222. FURI_LOG_E(TAG, "fhttp is NULL");
  223. return false;
  224. }
  225. if (fhttp->state == ISSUE)
  226. {
  227. FURI_LOG_E(TAG, "Failed to fetch last app update");
  228. return false;
  229. }
  230. char version_str[32];
  231. if (!flipper_server)
  232. {
  233. if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
  234. {
  235. FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
  236. return false;
  237. }
  238. char *app_version = get_json_value("version", fhttp->last_response);
  239. if (app_version)
  240. {
  241. // Save the server app version: it should save something like: 0.8
  242. save_char("server_app_version", app_version);
  243. snprintf(version_str, sizeof(version_str), "%s", app_version);
  244. free(app_version);
  245. }
  246. else
  247. {
  248. FURI_LOG_E(TAG, "Failed to get app version");
  249. return false;
  250. }
  251. }
  252. else
  253. {
  254. FuriString *app_data = flipper_http_load_from_file_with_limit(fhttp->file_path, memmgr_heap_get_max_free_block());
  255. if (!app_data)
  256. {
  257. FURI_LOG_E(TAG, "Failed to load app data");
  258. return false;
  259. }
  260. FuriString *current_version = get_json_value_furi("current_version", app_data);
  261. if (!current_version)
  262. {
  263. FURI_LOG_E(TAG, "Failed to get current version");
  264. furi_string_free(app_data);
  265. return false;
  266. }
  267. furi_string_free(app_data);
  268. FuriString *version = get_json_value_furi("version", current_version);
  269. if (!version)
  270. {
  271. FURI_LOG_E(TAG, "Failed to get version");
  272. furi_string_free(current_version);
  273. furi_string_free(app_data);
  274. return false;
  275. }
  276. // Save the server app version: it should save something like: 0.8
  277. save_char("server_app_version", furi_string_get_cstr(version));
  278. snprintf(version_str, sizeof(version_str), "%s", furi_string_get_cstr(version));
  279. furi_string_free(current_version);
  280. furi_string_free(version);
  281. // furi_string_free(app_data);
  282. }
  283. // Only check for an update if an hour or more has passed.
  284. if (flip_world_is_update_time(time_current))
  285. {
  286. char app_version[32];
  287. if (!load_char("app_version", app_version, sizeof(app_version)))
  288. {
  289. FURI_LOG_E(TAG, "Failed to load app version");
  290. return false;
  291. }
  292. FURI_LOG_I(TAG, "App version: %s", app_version);
  293. FURI_LOG_I(TAG, "Server version: %s", version_str);
  294. // Check if the app version is different from the server version.
  295. if (!is_str(app_version, version_str))
  296. {
  297. easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
  298. return true; // Update available.
  299. }
  300. FURI_LOG_I(TAG, "No update available");
  301. return false; // No update available.
  302. }
  303. FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
  304. return false; // Not yet time to update.
  305. }
  306. static bool flip_world_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
  307. {
  308. if (!fhttp)
  309. {
  310. FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
  311. return false;
  312. }
  313. char url[256];
  314. fhttp->save_received_data = false;
  315. fhttp->is_bytes_request = true;
  316. #ifndef FW_ORIGIN_Momentum
  317. snprintf(
  318. fhttp->file_path,
  319. sizeof(fhttp->file_path),
  320. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/flip_world.fap");
  321. #else
  322. snprintf(
  323. fhttp->file_path,
  324. sizeof(fhttp->file_path),
  325. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/FlipperHTTP/flip_world.fap");
  326. #endif
  327. if (flipper_server)
  328. {
  329. char build_id[32];
  330. snprintf(build_id, sizeof(build_id), "%s", BUILD_ID);
  331. uint8_t target;
  332. target = furi_hal_version_get_hw_target();
  333. uint16_t api_major, api_minor;
  334. furi_hal_info_get_api_version(&api_major, &api_minor);
  335. snprintf(
  336. url,
  337. sizeof(url),
  338. "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
  339. build_id,
  340. target,
  341. api_major,
  342. api_minor);
  343. }
  344. else
  345. {
  346. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/flip_world/");
  347. }
  348. return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
  349. }
  350. // Updates the app. Uses the supplied current time for validating if update check should proceed.
  351. static bool flip_world_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
  352. {
  353. if (!fhttp)
  354. {
  355. FURI_LOG_E(TAG, "fhttp is NULL");
  356. return false;
  357. }
  358. if (!flip_world_last_app_update(fhttp, use_flipper_api))
  359. {
  360. FURI_LOG_E(TAG, "Failed to fetch last app update");
  361. return false;
  362. }
  363. fhttp->state = RECEIVING;
  364. furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
  365. while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
  366. {
  367. furi_delay_ms(100);
  368. }
  369. furi_timer_stop(fhttp->get_timeout_timer);
  370. if (flip_world_parse_last_app_update(fhttp, time_current, use_flipper_api))
  371. {
  372. if (!flip_world_get_fap_file(fhttp, false))
  373. {
  374. FURI_LOG_E(TAG, "Failed to fetch fap file 1");
  375. return false;
  376. }
  377. fhttp->state = RECEIVING;
  378. while (fhttp->state == RECEIVING)
  379. {
  380. furi_delay_ms(100);
  381. }
  382. if (fhttp->state == ISSUE)
  383. {
  384. FURI_LOG_E(TAG, "Failed to fetch fap file 2");
  385. easy_flipper_dialog("Update Error", "Failed to download the\nupdate file.\nPlease try again.");
  386. return false;
  387. }
  388. return true;
  389. }
  390. FURI_LOG_I(TAG, "No update available");
  391. return false; // No update available.
  392. }
  393. // Handles the app update routine. This function obtains the current RTC time,
  394. // checks the "last_checked" value, and if it is more than one hour old, calls for an update.
  395. bool flip_world_handle_app_update(FlipperHTTP *fhttp, bool use_flipper_api)
  396. {
  397. if (!fhttp)
  398. {
  399. FURI_LOG_E(TAG, "fhttp is NULL");
  400. return false;
  401. }
  402. DateTime rtc_time;
  403. furi_hal_rtc_get_datetime(&rtc_time);
  404. char last_checked[32];
  405. if (!load_char("last_checked", last_checked, sizeof(last_checked)))
  406. {
  407. // First time – save the current time and check for an update.
  408. if (!flip_world_save_rtc_time(&rtc_time))
  409. {
  410. FURI_LOG_E(TAG, "Failed to save RTC time");
  411. return false;
  412. }
  413. return flip_world_update_app(fhttp, &rtc_time, use_flipper_api);
  414. }
  415. else
  416. {
  417. // Check if the current RTC time is at least one hour past the stored time.
  418. if (flip_world_is_update_time(&rtc_time))
  419. {
  420. if (!flip_world_update_app(fhttp, &rtc_time, use_flipper_api))
  421. {
  422. FURI_LOG_E(TAG, "Failed to update app");
  423. // save the current time for the next check.
  424. if (!flip_world_save_rtc_time(&rtc_time))
  425. {
  426. FURI_LOG_E(TAG, "Failed to save RTC time");
  427. return false;
  428. }
  429. return false;
  430. }
  431. // Save the current time for the next check.
  432. if (!flip_world_save_rtc_time(&rtc_time))
  433. {
  434. FURI_LOG_E(TAG, "Failed to save RTC time");
  435. return false;
  436. }
  437. return true;
  438. }
  439. return false; // No update necessary.
  440. }
  441. }