flip_world.c 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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_checked 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_checked_old[128];
  138. if (!load_char("last_checked", last_checked_old, sizeof(last_checked_old)))
  139. {
  140. FURI_LOG_E(TAG, "Failed to load last_checked");
  141. FuriString *json = flip_world_datetime_to_json(time_current);
  142. if (json)
  143. {
  144. save_char("last_checked", 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_checked_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 = false;
  206. fhttp->is_bytes_request = true;
  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://raw.githubusercontent.com/flipperdevices/flipper-application-catalog/main/applications/GPIO/flip_world/manifest.yml");
  209. return flipper_http_request(fhttp, BYTES, 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. /*
  218. * Scans a NUL‐terminated YAML buffer for a top‐level “version:” key,
  219. * and writes its (unquoted) value into out_version.
  220. * Returns true on success.
  221. */
  222. static bool parse_yaml_version(const char *yaml, char *out_version, size_t out_len)
  223. {
  224. const char *p = strstr(yaml, "\nversion:");
  225. if (!p)
  226. {
  227. // maybe it's the very first line
  228. p = yaml;
  229. }
  230. else
  231. {
  232. // skip the “\n”
  233. p++;
  234. }
  235. // skip the key name and colon
  236. p = strstr(p, "version");
  237. if (!p)
  238. return false;
  239. p += strlen("version");
  240. // skip whitespace and colon
  241. while (*p == ' ' || *p == ':')
  242. p++;
  243. // handle optional quote
  244. bool quoted = (*p == '"');
  245. if (quoted)
  246. p++;
  247. // copy up until end‐quote or newline/space
  248. size_t i = 0;
  249. while (*p && i + 1 < out_len)
  250. {
  251. if ((quoted && *p == '"') ||
  252. (!quoted && (*p == '\n' || *p == ' ')))
  253. {
  254. break;
  255. }
  256. out_version[i++] = *p++;
  257. }
  258. out_version[i] = '\0';
  259. return (i > 0);
  260. }
  261. // Parses the server response and returns true if an update is available.
  262. static bool flip_world_parse_last_app_update(FlipperHTTP *fhttp, DateTime *time_current, bool flipper_server)
  263. {
  264. if (!fhttp)
  265. {
  266. FURI_LOG_E(TAG, "fhttp is NULL");
  267. return false;
  268. }
  269. if (fhttp->state == ISSUE)
  270. {
  271. FURI_LOG_E(TAG, "Failed to fetch last app update");
  272. return false;
  273. }
  274. char version_str[32];
  275. if (!flipper_server)
  276. {
  277. if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0)
  278. {
  279. FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty");
  280. return false;
  281. }
  282. char *app_version = get_json_value("version", fhttp->last_response);
  283. if (app_version)
  284. {
  285. // Save the server app version: it should save something like: 0.8
  286. save_char("server_app_version", app_version);
  287. snprintf(version_str, sizeof(version_str), "%s", app_version);
  288. free(app_version);
  289. }
  290. else
  291. {
  292. FURI_LOG_E(TAG, "Failed to get app version");
  293. return false;
  294. }
  295. }
  296. else
  297. {
  298. FuriString *manifest_data = flipper_http_load_from_file(fhttp->file_path);
  299. if (!manifest_data)
  300. {
  301. FURI_LOG_E(TAG, "Failed to load app data");
  302. return false;
  303. }
  304. // parse version out of the YAML
  305. if (!parse_yaml_version(furi_string_get_cstr(manifest_data), version_str, sizeof(version_str)))
  306. {
  307. FURI_LOG_E(TAG, "Failed to parse version from YAML manifest");
  308. return false;
  309. }
  310. save_char("server_app_version", version_str);
  311. furi_string_free(manifest_data);
  312. }
  313. // Only check for an update if an hour or more has passed.
  314. if (flip_world_is_update_time(time_current))
  315. {
  316. char app_version[32];
  317. if (!load_char("app_version", app_version, sizeof(app_version)))
  318. {
  319. FURI_LOG_E(TAG, "Failed to load app version");
  320. return false;
  321. }
  322. // Check if the app version is different from the server version.
  323. if (!is_str(app_version, version_str))
  324. {
  325. easy_flipper_dialog("Update available", "New update available!\nPress BACK to download.");
  326. return true; // Update available.
  327. }
  328. FURI_LOG_I(TAG, "No update available");
  329. return false; // No update available.
  330. }
  331. FURI_LOG_I(TAG, "Not enough time has passed since the last update check");
  332. return false; // Not yet time to update.
  333. }
  334. static bool flip_world_get_fap_file(FlipperHTTP *fhttp, bool flipper_server)
  335. {
  336. if (!fhttp)
  337. {
  338. FURI_LOG_E(TAG, "FlipperHTTP is NULL.");
  339. return false;
  340. }
  341. char url[256];
  342. fhttp->save_received_data = false;
  343. fhttp->is_bytes_request = true;
  344. #ifndef FW_ORIGIN_Momentum
  345. snprintf(
  346. fhttp->file_path,
  347. sizeof(fhttp->file_path),
  348. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/flip_world.fap");
  349. #else
  350. snprintf(
  351. fhttp->file_path,
  352. sizeof(fhttp->file_path),
  353. STORAGE_EXT_PATH_PREFIX "/apps/GPIO/FlipperHTTP/flip_world.fap");
  354. #endif
  355. if (flipper_server)
  356. {
  357. char build_id[32];
  358. snprintf(build_id, sizeof(build_id), "%s", BUILD_ID);
  359. uint8_t target;
  360. target = furi_hal_version_get_hw_target();
  361. uint16_t api_major, api_minor;
  362. furi_hal_info_get_api_version(&api_major, &api_minor);
  363. snprintf(
  364. url,
  365. sizeof(url),
  366. "https://catalog.flipperzero.one/api/v0/application/version/%s/build/compatible?target=f%d&api=%d.%d",
  367. build_id,
  368. target,
  369. api_major,
  370. api_minor);
  371. }
  372. else
  373. {
  374. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/app/download/flip_world/");
  375. }
  376. return flipper_http_request(fhttp, BYTES, url, "{\"Content-Type\": \"application/octet-stream\"}", NULL);
  377. }
  378. // Updates the app. Uses the supplied current time for validating if update check should proceed.
  379. static bool flip_world_update_app(FlipperHTTP *fhttp, DateTime *time_current, bool use_flipper_api)
  380. {
  381. if (!fhttp)
  382. {
  383. FURI_LOG_E(TAG, "fhttp is NULL");
  384. return false;
  385. }
  386. if (!flip_world_last_app_update(fhttp, use_flipper_api))
  387. {
  388. FURI_LOG_E(TAG, "Failed to fetch last app update");
  389. return false;
  390. }
  391. fhttp->state = RECEIVING;
  392. furi_timer_start(fhttp->get_timeout_timer, TIMEOUT_DURATION_TICKS);
  393. while (fhttp->state == RECEIVING && furi_timer_is_running(fhttp->get_timeout_timer) > 0)
  394. {
  395. furi_delay_ms(100);
  396. }
  397. furi_timer_stop(fhttp->get_timeout_timer);
  398. if (flip_world_parse_last_app_update(fhttp, time_current, use_flipper_api))
  399. {
  400. if (!flip_world_get_fap_file(fhttp, false))
  401. {
  402. FURI_LOG_E(TAG, "Failed to fetch fap file 1");
  403. return false;
  404. }
  405. fhttp->state = RECEIVING;
  406. while (fhttp->state == RECEIVING)
  407. {
  408. furi_delay_ms(100);
  409. }
  410. if (fhttp->state == ISSUE)
  411. {
  412. FURI_LOG_E(TAG, "Failed to fetch fap file 2");
  413. easy_flipper_dialog("Update Error", "Failed to download the\nupdate file.\nPlease try again.");
  414. return false;
  415. }
  416. return true;
  417. }
  418. return false; // No update available.
  419. }
  420. // Handles the app update routine. This function obtains the current RTC time,
  421. // checks the "last_checked" value, and if it is more than one hour old, calls for an update.
  422. bool flip_world_handle_app_update(FlipperHTTP *fhttp, bool use_flipper_api)
  423. {
  424. if (!fhttp)
  425. {
  426. FURI_LOG_E(TAG, "fhttp is NULL");
  427. return false;
  428. }
  429. DateTime rtc_time;
  430. furi_hal_rtc_get_datetime(&rtc_time);
  431. char last_checked[32];
  432. if (!load_char("last_checked", last_checked, sizeof(last_checked)))
  433. {
  434. // First time – save the current time and check for an update.
  435. if (!flip_world_save_rtc_time(&rtc_time))
  436. {
  437. FURI_LOG_E(TAG, "Failed to save RTC time");
  438. return false;
  439. }
  440. return flip_world_update_app(fhttp, &rtc_time, use_flipper_api);
  441. }
  442. else
  443. {
  444. // Check if the current RTC time is at least one hour past the stored time.
  445. if (flip_world_is_update_time(&rtc_time))
  446. {
  447. if (!flip_world_update_app(fhttp, &rtc_time, use_flipper_api))
  448. {
  449. // save the last_checked for the next check.
  450. if (!flip_world_save_rtc_time(&rtc_time))
  451. {
  452. FURI_LOG_E(TAG, "Failed to save RTC time");
  453. return false;
  454. }
  455. return false;
  456. }
  457. // Save the current time for the next check.
  458. if (!flip_world_save_rtc_time(&rtc_time))
  459. {
  460. FURI_LOG_E(TAG, "Failed to save RTC time");
  461. return false;
  462. }
  463. return true;
  464. }
  465. return false; // No update necessary.
  466. }
  467. }