game.c 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. #include <callback/game.h>
  2. //
  3. #include "engine/engine.h"
  4. #include "engine/game_engine.h"
  5. #include "engine/game_manager_i.h"
  6. #include "engine/level_i.h"
  7. #include "engine/entity_i.h"
  8. //
  9. #include "game/storage.h"
  10. //
  11. #include <callback/loader.h>
  12. #include <callback/free.h>
  13. #include <callback/alloc.h>
  14. #include <callback/callback.h>
  15. #include "alloc/alloc.h"
  16. #include <flip_storage/storage.h>
  17. bool user_hit_back = false;
  18. uint32_t lobby_index = -1;
  19. char *lobby_list[10];
  20. static uint8_t timer_iteration = 0; // timer iteration for the loading screen
  21. static uint8_t timer_refresh = 5; // duration for timer to refresh
  22. FuriThread *game_thread = NULL;
  23. FuriThread *waiting_thread = NULL;
  24. bool game_thread_running = false;
  25. bool waiting_thread_running = false;
  26. static void game_frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context)
  27. {
  28. UNUSED(engine);
  29. GameManager *game_manager = context;
  30. game_manager_input_set(game_manager, input);
  31. game_manager_update(game_manager);
  32. game_manager_render(game_manager, canvas);
  33. }
  34. static int32_t game_app(void *p)
  35. {
  36. UNUSED(p);
  37. GameManager *game_manager = game_manager_alloc();
  38. if (!game_manager)
  39. {
  40. FURI_LOG_E("Game", "Failed to allocate game manager");
  41. return -1;
  42. }
  43. // Setup game engine settings...
  44. GameEngineSettings settings = game_engine_settings_init();
  45. settings.target_fps = atof_(fps_choices_str[fps_index]);
  46. settings.show_fps = game.show_fps;
  47. settings.always_backlight = strstr(yes_or_no_choices[screen_always_on_index], "Yes") != NULL;
  48. settings.frame_callback = game_frame_cb;
  49. settings.context = game_manager;
  50. GameEngine *engine = game_engine_alloc(settings);
  51. if (!engine)
  52. {
  53. FURI_LOG_E("Game", "Failed to allocate game engine");
  54. game_manager_free(game_manager);
  55. return -1;
  56. }
  57. game_manager_engine_set(game_manager, engine);
  58. // Allocate custom game context if needed
  59. void *game_context = NULL;
  60. if (game.context_size > 0)
  61. {
  62. game_context = malloc(game.context_size);
  63. game_manager_game_context_set(game_manager, game_context);
  64. }
  65. // Start the game
  66. game.start(game_manager, game_context);
  67. // 1) Run the engine
  68. game_engine_run(engine);
  69. // 2) Stop the game FIRST, so it can do any internal cleanup
  70. game.stop(game_context);
  71. // 3) Now free the engine
  72. game_engine_free(engine);
  73. // 4) Now free the manager
  74. game_manager_free(game_manager);
  75. // 5) Finally, free your custom context if it was allocated
  76. if (game_context)
  77. {
  78. free(game_context);
  79. }
  80. // 6) Check for leftover entities
  81. int32_t entities = entities_get_count();
  82. if (entities != 0)
  83. {
  84. FURI_LOG_E("Game", "Memory leak detected: %ld entities still allocated", entities);
  85. return -1;
  86. }
  87. return 0;
  88. }
  89. static int32_t game_waiting_app_callback(void *p)
  90. {
  91. FlipWorldApp *app = (FlipWorldApp *)p;
  92. furi_check(app);
  93. FlipperHTTP *fhttp = flipper_http_alloc();
  94. if (!fhttp)
  95. {
  96. FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
  97. easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP");
  98. return -1;
  99. }
  100. user_hit_back = false;
  101. timer_iteration = 0;
  102. while (timer_iteration < 60 && !user_hit_back)
  103. {
  104. FURI_LOG_I(TAG, "Waiting for more players...");
  105. game_waiting_process(fhttp, app);
  106. FURI_LOG_I(TAG, "Waiting for more players... %d", timer_iteration);
  107. timer_iteration++;
  108. furi_delay_ms(1000 * timer_refresh);
  109. }
  110. // if we reach here, it means we timed out or the user hit back
  111. FURI_LOG_E(TAG, "No players joined within the timeout or user hit back");
  112. remove_player_from_lobby(fhttp);
  113. flipper_http_free(fhttp);
  114. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
  115. return 0;
  116. }
  117. static bool game_start_waiting_thread(void *context)
  118. {
  119. FlipWorldApp *app = (FlipWorldApp *)context;
  120. furi_check(app);
  121. // free game thread
  122. if (waiting_thread_running)
  123. {
  124. waiting_thread_running = false;
  125. if (waiting_thread)
  126. {
  127. furi_thread_flags_set(furi_thread_get_id(waiting_thread), WorkerEvtStop);
  128. furi_thread_join(waiting_thread);
  129. furi_thread_free(waiting_thread);
  130. }
  131. }
  132. // start waiting thread
  133. FuriThread *thread = furi_thread_alloc_ex("waiting_thread", 2048, game_waiting_app_callback, app);
  134. if (!thread)
  135. {
  136. FURI_LOG_E(TAG, "Failed to allocate waiting thread");
  137. easy_flipper_dialog("Error", "Failed to allocate waiting thread. Restart your Flipper.");
  138. return false;
  139. }
  140. furi_thread_start(thread);
  141. waiting_thread = thread;
  142. waiting_thread_running = true;
  143. return true;
  144. }
  145. static bool game_fetch_world_list(FlipperHTTP *fhttp)
  146. {
  147. if (!fhttp)
  148. {
  149. FURI_LOG_E(TAG, "fhttp is NULL");
  150. easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
  151. return false;
  152. }
  153. // ensure flip_world directory exists
  154. char directory_path[128];
  155. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
  156. Storage *storage = furi_record_open(RECORD_STORAGE);
  157. storage_common_mkdir(storage, directory_path);
  158. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
  159. storage_common_mkdir(storage, directory_path);
  160. furi_record_close(RECORD_STORAGE);
  161. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
  162. fhttp->save_received_data = true;
  163. return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/v5/list/10/", "{\"Content-Type\":\"application/json\"}", NULL);
  164. }
  165. // we will load the palyer stats from the API and save them
  166. // in player_spawn game method, it will load the player stats that we saved
  167. static bool game_fetch_player_stats(FlipperHTTP *fhttp)
  168. {
  169. if (!fhttp)
  170. {
  171. FURI_LOG_E(TAG, "fhttp is NULL");
  172. easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
  173. return false;
  174. }
  175. char username[64];
  176. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  177. {
  178. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  179. easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update.");
  180. return false;
  181. }
  182. char url[128];
  183. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/user/game-stats/%s/", username);
  184. // ensure the folders exist
  185. char directory_path[128];
  186. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
  187. Storage *storage = furi_record_open(RECORD_STORAGE);
  188. storage_common_mkdir(storage, directory_path);
  189. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data");
  190. storage_common_mkdir(storage, directory_path);
  191. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player");
  192. storage_common_mkdir(storage, directory_path);
  193. furi_record_close(RECORD_STORAGE);
  194. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json");
  195. fhttp->save_received_data = true;
  196. return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
  197. }
  198. static bool game_thread_start(void *context)
  199. {
  200. FlipWorldApp *app = (FlipWorldApp *)context;
  201. if (!app)
  202. {
  203. FURI_LOG_E(TAG, "app is NULL");
  204. easy_flipper_dialog("Error", "app is NULL. Press BACK to return.");
  205. return false;
  206. }
  207. // free everything but message_view
  208. free_variable_item_list(app);
  209. free_text_input_view(app);
  210. // free_submenu_other(app); // free lobby list or settings
  211. loader_view_free(app);
  212. free_game_submenu(app);
  213. // free game thread
  214. if (game_thread_running)
  215. {
  216. game_thread_running = false;
  217. if (game_thread)
  218. {
  219. furi_thread_flags_set(furi_thread_get_id(game_thread), WorkerEvtStop);
  220. furi_thread_join(game_thread);
  221. furi_thread_free(game_thread);
  222. }
  223. }
  224. // start game thread
  225. FuriThread *thread = furi_thread_alloc_ex("game", 2048, game_app, app);
  226. if (!thread)
  227. {
  228. FURI_LOG_E(TAG, "Failed to allocate game thread");
  229. easy_flipper_dialog("Error", "Failed to allocate game thread. Restart your Flipper.");
  230. return false;
  231. }
  232. furi_thread_start(thread);
  233. game_thread = thread;
  234. game_thread_running = true;
  235. return true;
  236. }
  237. // combine register, login, and world list fetch into one function to switch to the loader view
  238. static bool game_fetch(DataLoaderModel *model)
  239. {
  240. FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
  241. if (!app)
  242. {
  243. FURI_LOG_E(TAG, "app is NULL");
  244. easy_flipper_dialog("Error", "app is NULL. Press BACK to return.");
  245. return false;
  246. }
  247. if (model->request_index == 0)
  248. {
  249. // login
  250. char username[64];
  251. char password[64];
  252. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  253. {
  254. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  255. view_dispatcher_switch_to_view(app->view_dispatcher,
  256. FlipWorldViewSubmenu); // just go back to the main menu for now
  257. easy_flipper_dialog("Error", "Failed to load saved username\nGo to user settings to update.");
  258. return false;
  259. }
  260. if (!load_char("Flip-Social-Password", password, sizeof(password)))
  261. {
  262. FURI_LOG_E(TAG, "Failed to load Flip-Social-Password");
  263. view_dispatcher_switch_to_view(app->view_dispatcher,
  264. FlipWorldViewSubmenu); // just go back to the main menu for now
  265. easy_flipper_dialog("Error", "Failed to load saved password\nGo to settings to update.");
  266. return false;
  267. }
  268. char payload[256];
  269. snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password);
  270. return flipper_http_request(model->fhttp, POST, "https://www.jblanked.com/flipper/api/user/login/", "{\"Content-Type\":\"application/json\"}", payload);
  271. }
  272. else if (model->request_index == 1)
  273. {
  274. // check if login was successful
  275. char is_logged_in[8];
  276. if (!load_char("is_logged_in", is_logged_in, sizeof(is_logged_in)))
  277. {
  278. FURI_LOG_E(TAG, "Failed to load is_logged_in");
  279. easy_flipper_dialog("Error", "Failed to load is_logged_in\nGo to user settings to update.");
  280. view_dispatcher_switch_to_view(app->view_dispatcher,
  281. FlipWorldViewSubmenu); // just go back to the main menu for now
  282. return false;
  283. }
  284. if (is_str(is_logged_in, "false") && is_str(model->title, "Registering..."))
  285. {
  286. // register
  287. char username[64];
  288. char password[64];
  289. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  290. {
  291. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  292. easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update.");
  293. view_dispatcher_switch_to_view(app->view_dispatcher,
  294. FlipWorldViewSubmenu); // just go back to the main menu for now
  295. return false;
  296. }
  297. if (!load_char("Flip-Social-Password", password, sizeof(password)))
  298. {
  299. FURI_LOG_E(TAG, "Failed to load Flip-Social-Password");
  300. easy_flipper_dialog("Error", "Failed to load saved password. Go to settings to update.");
  301. view_dispatcher_switch_to_view(app->view_dispatcher,
  302. FlipWorldViewSubmenu); // just go back to the main menu for now
  303. return false;
  304. }
  305. char payload[172];
  306. snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password);
  307. model->title = "Registering...";
  308. return flipper_http_request(model->fhttp, POST, "https://www.jblanked.com/flipper/api/user/register/", "{\"Content-Type\":\"application/json\"}", payload);
  309. }
  310. else
  311. {
  312. model->title = "Fetching World List..";
  313. return game_fetch_world_list(model->fhttp);
  314. }
  315. }
  316. else if (model->request_index == 2)
  317. {
  318. model->title = "Fetching World List..";
  319. return game_fetch_world_list(model->fhttp);
  320. }
  321. else if (model->request_index == 3)
  322. {
  323. snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
  324. FuriString *world_list = flipper_http_load_from_file(model->fhttp->file_path);
  325. if (!world_list)
  326. {
  327. view_dispatcher_switch_to_view(app->view_dispatcher,
  328. FlipWorldViewSubmenu); // just go back to the main menu for now
  329. FURI_LOG_E(TAG, "Failed to load world list");
  330. easy_flipper_dialog("Error", "Failed to load world list. Go to game settings to download packs.");
  331. return false;
  332. }
  333. FuriString *first_world = get_json_array_value_furi("worlds", 0, world_list);
  334. if (!first_world)
  335. {
  336. view_dispatcher_switch_to_view(app->view_dispatcher,
  337. FlipWorldViewSubmenu); // just go back to the main menu for now
  338. FURI_LOG_E(TAG, "Failed to get first world");
  339. easy_flipper_dialog("Error", "Failed to get first world. Go to game settings to download packs.");
  340. furi_string_free(world_list);
  341. return false;
  342. }
  343. if (world_exists(furi_string_get_cstr(first_world)))
  344. {
  345. furi_string_free(world_list);
  346. furi_string_free(first_world);
  347. if (!game_thread_start(app))
  348. {
  349. FURI_LOG_E(TAG, "Failed to start game thread");
  350. easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
  351. view_dispatcher_switch_to_view(app->view_dispatcher,
  352. FlipWorldViewSubmenu); // just go back to the main menu for now
  353. return "Failed to start game thread";
  354. }
  355. return true;
  356. }
  357. snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(first_world));
  358. model->fhttp->save_received_data = true;
  359. char url[128];
  360. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/v5/get/world/%s/", furi_string_get_cstr(first_world));
  361. furi_string_free(world_list);
  362. furi_string_free(first_world);
  363. return flipper_http_request(model->fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL);
  364. }
  365. FURI_LOG_E(TAG, "Unknown request index");
  366. return false;
  367. }
  368. static char *game_parse(DataLoaderModel *model)
  369. {
  370. FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
  371. if (model->request_index == 0)
  372. {
  373. if (!model->fhttp->last_response)
  374. {
  375. save_char("is_logged_in", "false");
  376. // Go back to the main menu
  377. easy_flipper_dialog("Error", "Response is empty. Press BACK to return.");
  378. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  379. return "Response is empty...";
  380. }
  381. // Check for successful conditions
  382. if (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User found") != NULL)
  383. {
  384. save_char("is_logged_in", "true");
  385. model->title = "Login successful!";
  386. model->title = "Fetching World List..";
  387. return "Login successful!";
  388. }
  389. // Check if user not found
  390. if (strstr(model->fhttp->last_response, "User not found") != NULL)
  391. {
  392. save_char("is_logged_in", "false");
  393. model->title = "Registering...";
  394. return "Account not found...\nRegistering now.."; // if they see this an issue happened switching to register
  395. }
  396. // If not success, not found, check length conditions
  397. size_t resp_len = strlen(model->fhttp->last_response);
  398. if (resp_len == 0 || resp_len > 127)
  399. {
  400. // Empty or too long means failed login
  401. save_char("is_logged_in", "false");
  402. // Go back to the main menu
  403. easy_flipper_dialog("Error", "Failed to login. Press BACK to return.");
  404. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  405. return "Failed to login...";
  406. }
  407. // Handle any other unknown response as a failure
  408. save_char("is_logged_in", "false");
  409. // Go back to the main menu
  410. easy_flipper_dialog("Error", "Failed to login. Press BACK to return.");
  411. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  412. return "Failed to login...";
  413. }
  414. else if (model->request_index == 1)
  415. {
  416. if (is_str(model->title, "Registering..."))
  417. {
  418. // check registration response
  419. if (model->fhttp->last_response != NULL && (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User created") != NULL))
  420. {
  421. save_char("is_logged_in", "true");
  422. char username[64];
  423. char password[64];
  424. // load the username and password, then save them
  425. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  426. {
  427. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  428. easy_flipper_dialog("Error", "Failed to load Flip-Social-Username");
  429. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  430. return "Failed to load Flip-Social-Username";
  431. }
  432. if (!load_char("Flip-Social-Password", password, sizeof(password)))
  433. {
  434. FURI_LOG_E(TAG, "Failed to load Flip-Social-Password");
  435. easy_flipper_dialog("Error", "Failed to load Flip-Social-Password");
  436. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  437. return "Failed to load Flip-Social-Password";
  438. }
  439. // load wifi ssid,pass then save
  440. char ssid[64];
  441. char pass[64];
  442. if (!load_char("WiFi-SSID", ssid, sizeof(ssid)))
  443. {
  444. FURI_LOG_E(TAG, "Failed to load WiFi-SSID");
  445. easy_flipper_dialog("Error", "Failed to load WiFi-SSID");
  446. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  447. return "Failed to load WiFi-SSID";
  448. }
  449. if (!load_char("WiFi-Password", pass, sizeof(pass)))
  450. {
  451. FURI_LOG_E(TAG, "Failed to load WiFi-Password");
  452. easy_flipper_dialog("Error", "Failed to load WiFi-Password");
  453. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
  454. return "Failed to load WiFi-Password";
  455. }
  456. save_settings(ssid, pass, username, password);
  457. model->title = "Fetching World List..";
  458. return "Account created!";
  459. }
  460. else if (strstr(model->fhttp->last_response, "Username or password not provided") != NULL)
  461. {
  462. easy_flipper_dialog("Error", "Please enter your credentials.\nPress BACK to return.");
  463. view_dispatcher_switch_to_view(app->view_dispatcher,
  464. FlipWorldViewSubmenu); // just go back to the main menu for now
  465. return "Please enter your credentials.";
  466. }
  467. else if (strstr(model->fhttp->last_response, "User already exists") != NULL || strstr(model->fhttp->last_response, "Multiple users found") != NULL)
  468. {
  469. easy_flipper_dialog("Error", "Registration failed...\nUsername already exists.\nPress BACK to return.");
  470. view_dispatcher_switch_to_view(app->view_dispatcher,
  471. FlipWorldViewSubmenu); // just go back to the main menu for now
  472. return "Username already exists.";
  473. }
  474. else
  475. {
  476. easy_flipper_dialog("Error", "Registration failed...\nUpdate your credentials.\nPress BACK to return.");
  477. view_dispatcher_switch_to_view(app->view_dispatcher,
  478. FlipWorldViewSubmenu); // just go back to the main menu for now
  479. return "Registration failed...";
  480. }
  481. }
  482. else
  483. {
  484. if (!game_thread_start(app))
  485. {
  486. FURI_LOG_E(TAG, "Failed to start game thread");
  487. easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
  488. view_dispatcher_switch_to_view(app->view_dispatcher,
  489. FlipWorldViewSubmenu); // just go back to the main menu for now
  490. return "Failed to start game thread";
  491. }
  492. return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close.";
  493. }
  494. }
  495. else if (model->request_index == 2)
  496. {
  497. return "Welcome to FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close.";
  498. }
  499. else if (model->request_index == 3)
  500. {
  501. if (!game_thread_start(app))
  502. {
  503. FURI_LOG_E(TAG, "Failed to start game thread");
  504. easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
  505. view_dispatcher_switch_to_view(app->view_dispatcher,
  506. FlipWorldViewSubmenu); // just go back to the main menu for now
  507. return "Failed to start game thread";
  508. }
  509. return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close.";
  510. }
  511. easy_flipper_dialog("Error", "Unknown error. Press BACK to return.");
  512. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
  513. return "Unknown error";
  514. }
  515. static void game_switch_to_view(FlipWorldApp *app)
  516. {
  517. if (!loader_view_alloc(app))
  518. {
  519. FURI_LOG_E(TAG, "Failed to allocate view loader");
  520. return;
  521. }
  522. loader_switch_to_view(app, "Starting Game..", game_fetch, game_parse, 5, callback_to_submenu, FlipWorldViewLoader);
  523. }
  524. void game_run(FlipWorldApp *app)
  525. {
  526. furi_check(app, "FlipWorldApp is NULL");
  527. free_all_views(app, true, true, false);
  528. // only need to check if they have 30k free (game needs about 12k currently)
  529. if (!is_enough_heap(30000, false))
  530. {
  531. const size_t min_free = memmgr_get_free_heap();
  532. char message[64];
  533. snprintf(message, sizeof(message), "Not enough heap memory.\nThere are %zu bytes free.", min_free);
  534. easy_flipper_dialog("Error", message);
  535. return;
  536. }
  537. // check if logged in
  538. if (is_logged_in() || is_logged_in_to_flip_social())
  539. {
  540. FlipperHTTP *fhttp = flipper_http_alloc();
  541. if (!fhttp)
  542. {
  543. FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
  544. easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
  545. return;
  546. }
  547. bool game_fetch_world_list_i()
  548. {
  549. return game_fetch_world_list(fhttp);
  550. }
  551. bool parse_world_list_i()
  552. {
  553. return fhttp->state != ISSUE;
  554. }
  555. bool game_fetch_player_stats_i()
  556. {
  557. return game_fetch_player_stats(fhttp);
  558. }
  559. if (!alloc_message_view(app, MessageStateLoading))
  560. {
  561. FURI_LOG_E(TAG, "Failed to allocate message view");
  562. return;
  563. }
  564. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
  565. // Make the request
  566. if (game_mode_index != 1) // not GAME_MODE_PVP
  567. {
  568. if (!flipper_http_process_response_async(fhttp, game_fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, game_fetch_player_stats_i, set_player_context))
  569. {
  570. FURI_LOG_E(HTTP_TAG, "Failed to make request");
  571. flipper_http_free(fhttp);
  572. }
  573. else
  574. {
  575. flipper_http_free(fhttp);
  576. }
  577. if (!game_thread_start(app))
  578. {
  579. FURI_LOG_E(TAG, "Failed to start game thread");
  580. easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
  581. return;
  582. }
  583. }
  584. else
  585. {
  586. // load pvp info (this returns the lobbies available)
  587. bool fetch_pvp_lobbies()
  588. {
  589. // ensure flip_world directory exists
  590. char directory_path[128];
  591. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
  592. Storage *storage = furi_record_open(RECORD_STORAGE);
  593. storage_common_mkdir(storage, directory_path);
  594. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp");
  595. storage_common_mkdir(storage, directory_path);
  596. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies");
  597. storage_common_mkdir(storage, directory_path);
  598. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/pvp_lobbies.json");
  599. storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty
  600. furi_record_close(RECORD_STORAGE);
  601. fhttp->save_received_data = true;
  602. // 2 players max, 10 lobbies
  603. return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/pvp/lobbies/2/10/", "{\"Content-Type\":\"application/json\"}", NULL);
  604. }
  605. bool parse_pvp_lobbies()
  606. {
  607. free_submenu_other(app);
  608. if (!alloc_submenu_other(app, FlipWorldViewLobby))
  609. {
  610. FURI_LOG_E(TAG, "Failed to allocate lobby submenu");
  611. return false;
  612. }
  613. // add the lobbies to the submenu
  614. FuriString *lobbies = flipper_http_load_from_file(fhttp->file_path);
  615. if (!lobbies)
  616. {
  617. FURI_LOG_E(TAG, "Failed to load lobbies");
  618. return false;
  619. }
  620. // parse the lobbies
  621. for (uint32_t i = 0; i < 10; i++)
  622. {
  623. FuriString *lobby = get_json_array_value_furi("lobbies", i, lobbies);
  624. if (!lobby)
  625. {
  626. FURI_LOG_I(TAG, "No more lobbies");
  627. break;
  628. }
  629. FuriString *lobby_id = get_json_value_furi("id", lobby);
  630. if (!lobby_id)
  631. {
  632. FURI_LOG_E(TAG, "Failed to get lobby id");
  633. furi_string_free(lobby);
  634. return false;
  635. }
  636. // add the lobby to the submenu
  637. submenu_add_item(app->submenu_other, furi_string_get_cstr(lobby_id), FlipWorldSubmenuIndexLobby + i, callback_submenu_lobby_choices, app);
  638. // add the lobby to the list
  639. if (!easy_flipper_set_buffer(&lobby_list[i], 64))
  640. {
  641. FURI_LOG_E(TAG, "Failed to allocate lobby list");
  642. furi_string_free(lobby);
  643. furi_string_free(lobby_id);
  644. return false;
  645. }
  646. snprintf(lobby_list[i], 64, "%s", furi_string_get_cstr(lobby_id));
  647. furi_string_free(lobby);
  648. furi_string_free(lobby_id);
  649. }
  650. furi_string_free(lobbies);
  651. return true;
  652. }
  653. // load pvp lobbies and player stats
  654. if (!flipper_http_process_response_async(fhttp, fetch_pvp_lobbies, parse_pvp_lobbies) || !flipper_http_process_response_async(fhttp, game_fetch_player_stats_i, set_player_context))
  655. {
  656. // unlike the pve/story, receiving data is necessary
  657. // so send the user back to the main menu if it fails
  658. FURI_LOG_E(HTTP_TAG, "Failed to make request");
  659. easy_flipper_dialog("Error", "Failed to make request. Press BACK to return.");
  660. view_dispatcher_switch_to_view(app->view_dispatcher,
  661. FlipWorldViewSubmenu);
  662. flipper_http_free(fhttp);
  663. }
  664. else
  665. {
  666. flipper_http_free(fhttp);
  667. }
  668. // switch to the lobby submenu
  669. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
  670. }
  671. }
  672. else
  673. {
  674. game_switch_to_view(app);
  675. }
  676. }
  677. bool game_fetch_lobby(FlipperHTTP *fhttp, char *lobby_name)
  678. {
  679. if (!fhttp)
  680. {
  681. FURI_LOG_E(TAG, "FlipperHTTP is NULL");
  682. return false;
  683. }
  684. if (!lobby_name || strlen(lobby_name) == 0)
  685. {
  686. FURI_LOG_E(TAG, "Lobby name is NULL or empty");
  687. return false;
  688. }
  689. char username[64];
  690. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  691. {
  692. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  693. return false;
  694. }
  695. // send the request to fetch the lobby details, with player_username
  696. char url[128];
  697. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/pvp/lobby/get/%s/%s/", lobby_name, username);
  698. snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies/%s.json", lobby_name);
  699. fhttp->save_received_data = true;
  700. if (!flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL))
  701. {
  702. FURI_LOG_E(TAG, "Failed to fetch lobby details");
  703. return false;
  704. }
  705. fhttp->state = RECEIVING;
  706. while (fhttp->state != IDLE)
  707. {
  708. furi_delay_ms(100);
  709. }
  710. return true;
  711. }
  712. bool game_join_lobby(FlipperHTTP *fhttp, char *lobby_name)
  713. {
  714. if (!fhttp)
  715. {
  716. FURI_LOG_E(TAG, "FlipperHTTP is NULL");
  717. return false;
  718. }
  719. if (!lobby_name || strlen(lobby_name) == 0)
  720. {
  721. FURI_LOG_E(TAG, "Lobby name is NULL or empty");
  722. return false;
  723. }
  724. char username[64];
  725. if (!load_char("Flip-Social-Username", username, sizeof(username)))
  726. {
  727. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  728. return false;
  729. }
  730. char url[128];
  731. char payload[128];
  732. snprintf(payload, sizeof(payload), "{\"username\":\"%s\", \"game_id\":\"%s\"}", username, lobby_name);
  733. save_char("pvp_lobby_name", lobby_name); // save the lobby name
  734. snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/pvp/lobby/join/");
  735. if (!flipper_http_request(fhttp, POST, url, "{\"Content-Type\":\"application/json\"}", payload))
  736. {
  737. FURI_LOG_E(TAG, "Failed to join lobby");
  738. return false;
  739. }
  740. fhttp->state = RECEIVING;
  741. while (fhttp->state != IDLE)
  742. {
  743. furi_delay_ms(100);
  744. }
  745. return true;
  746. }
  747. static bool game_create_pvp_enemy(FuriString *lobby_details)
  748. {
  749. if (!lobby_details)
  750. {
  751. FURI_LOG_E(TAG, "Failed to load lobby details");
  752. return false;
  753. }
  754. char current_user[64];
  755. if (!load_char("Flip-Social-Username", current_user, sizeof(current_user)))
  756. {
  757. FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
  758. save_char("create_pvp_error", "Failed to load Flip-Social-Username");
  759. return false;
  760. }
  761. for (uint8_t i = 0; i < 2; i++)
  762. {
  763. // parse the lobby details
  764. FuriString *player_stats = get_json_array_value_furi("player_stats", i, lobby_details);
  765. if (!player_stats)
  766. {
  767. FURI_LOG_E(TAG, "Failed to get player stats");
  768. save_char("create_pvp_error", "Failed to get player stats array");
  769. return false;
  770. }
  771. // available keys from player_stats
  772. FuriString *username = get_json_value_furi("username", player_stats);
  773. if (!username)
  774. {
  775. FURI_LOG_E(TAG, "Failed to get username");
  776. save_char("create_pvp_error", "Failed to get username");
  777. furi_string_free(player_stats);
  778. return false;
  779. }
  780. // check if the username is the same as the current user
  781. if (is_str(furi_string_get_cstr(username), current_user))
  782. {
  783. furi_string_free(player_stats);
  784. furi_string_free(username);
  785. continue; // skip the current user
  786. }
  787. FuriString *strength = get_json_value_furi("strength", player_stats);
  788. FuriString *health = get_json_value_furi("health", player_stats);
  789. FuriString *attack_timer = get_json_value_furi("attack_timer", player_stats);
  790. if (!strength || !health || !attack_timer)
  791. {
  792. FURI_LOG_E(TAG, "Failed to get player stats");
  793. save_char("create_pvp_error", "Failed to get player stats");
  794. furi_string_free(player_stats);
  795. furi_string_free(username);
  796. if (strength)
  797. furi_string_free(strength);
  798. if (health)
  799. furi_string_free(health);
  800. if (attack_timer)
  801. furi_string_free(attack_timer);
  802. return false;
  803. }
  804. // create enemy data
  805. FuriString *enemy_data = furi_string_alloc();
  806. furi_string_printf(
  807. enemy_data,
  808. "{\"enemy_data\":[{\"id\":\"sword\",\"is_user\":\"true\",\"username\":\"%s\","
  809. "\"index\":0,\"start_position\":{\"x\":350,\"y\":210},\"end_position\":{\"x\":350,\"y\":210},"
  810. "\"move_timer\":1,\"speed\":1,\"attack_timer\":%f,\"strength\":%f,\"health\":%f}]}",
  811. furi_string_get_cstr(username),
  812. (double)atof_furi(attack_timer),
  813. (double)atof_furi(strength),
  814. (double)atof_furi(health));
  815. char directory_path[128];
  816. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world");
  817. Storage *storage = furi_record_open(RECORD_STORAGE);
  818. storage_common_mkdir(storage, directory_path);
  819. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
  820. storage_common_mkdir(storage, directory_path);
  821. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/pvp_world");
  822. storage_common_mkdir(storage, directory_path);
  823. furi_record_close(RECORD_STORAGE);
  824. snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/pvp_world/pvp_world_enemy_data.json");
  825. // remove the enemy_data file if it exists
  826. storage_simply_remove_recursive(storage, directory_path);
  827. File *file = storage_file_alloc(storage);
  828. if (!storage_file_open(file, directory_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
  829. {
  830. FURI_LOG_E("Game", "Failed to open file for writing: %s", directory_path);
  831. save_char("create_pvp_error", "Failed to open file for writing");
  832. storage_file_free(file);
  833. furi_record_close(RECORD_STORAGE);
  834. furi_string_free(enemy_data);
  835. furi_string_free(player_stats);
  836. furi_string_free(username);
  837. furi_string_free(strength);
  838. furi_string_free(health);
  839. furi_string_free(attack_timer);
  840. return false;
  841. }
  842. size_t data_size = furi_string_size(enemy_data);
  843. if (storage_file_write(file, furi_string_get_cstr(enemy_data), data_size) != data_size)
  844. {
  845. FURI_LOG_E("Game", "Failed to write enemy_data");
  846. save_char("create_pvp_error", "Failed to write enemy_data");
  847. }
  848. storage_file_close(file);
  849. furi_string_free(enemy_data);
  850. furi_string_free(player_stats);
  851. furi_string_free(username);
  852. furi_string_free(strength);
  853. furi_string_free(health);
  854. furi_string_free(attack_timer);
  855. // player is found so break
  856. break;
  857. }
  858. return true;
  859. }
  860. size_t game_lobby_count(FlipperHTTP *fhttp, FuriString *lobby)
  861. {
  862. if (!fhttp)
  863. {
  864. FURI_LOG_E(TAG, "FlipperHTTP is NULL");
  865. return -1;
  866. }
  867. if (!lobby)
  868. {
  869. FURI_LOG_E(TAG, "Lobby details are NULL");
  870. return -1;
  871. }
  872. // check if the player is in the lobby
  873. FuriString *player_count = get_json_value_furi("player_count", lobby);
  874. if (!player_count)
  875. {
  876. FURI_LOG_E(TAG, "Failed to get player count");
  877. return -1;
  878. }
  879. const size_t count = atoi(furi_string_get_cstr(player_count));
  880. furi_string_free(player_count);
  881. return count;
  882. }
  883. bool game_in_lobby(FlipperHTTP *fhttp, FuriString *lobby)
  884. {
  885. if (!fhttp)
  886. {
  887. FURI_LOG_E(TAG, "FlipperHTTP is NULL");
  888. return false;
  889. }
  890. if (!lobby)
  891. {
  892. FURI_LOG_E(TAG, "Lobby details are NULL");
  893. return false;
  894. }
  895. // check if the player is in the lobby
  896. FuriString *is_in_game = get_json_value_furi("is_in_game", lobby);
  897. if (!is_in_game)
  898. {
  899. FURI_LOG_E(TAG, "Failed to get is_in_game");
  900. furi_string_free(is_in_game);
  901. return false;
  902. }
  903. const bool in_game = is_str(furi_string_get_cstr(is_in_game), "true");
  904. furi_string_free(is_in_game);
  905. return in_game;
  906. }
  907. static bool game_start_ws(FlipperHTTP *fhttp, char *lobby_name)
  908. {
  909. if (!fhttp)
  910. {
  911. FURI_LOG_E(TAG, "FlipperHTTP is NULL");
  912. return false;
  913. }
  914. if (!lobby_name || strlen(lobby_name) == 0)
  915. {
  916. FURI_LOG_E(TAG, "Lobby name is NULL or empty");
  917. return false;
  918. }
  919. fhttp->state = IDLE; // ensure it's set to IDLE for the next request
  920. char websocket_url[128];
  921. snprintf(websocket_url, sizeof(websocket_url), "ws://www.jblanked.com/ws/game/%s/", lobby_name);
  922. if (!flipper_http_websocket_start(fhttp, websocket_url, 80, "{\"Content-Type\":\"application/json\"}"))
  923. {
  924. FURI_LOG_E(TAG, "Failed to start websocket");
  925. return false;
  926. }
  927. fhttp->state = RECEIVING;
  928. while (fhttp->state != IDLE)
  929. {
  930. furi_delay_ms(100);
  931. }
  932. return true;
  933. }
  934. // this will free both the fhttp and lobby
  935. void game_start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context)
  936. {
  937. FlipWorldApp *app = (FlipWorldApp *)context;
  938. furi_check(app, "FlipWorldApp is NULL");
  939. // only thing left to do is create the enemy data and start the websocket session
  940. if (!game_create_pvp_enemy(lobby))
  941. {
  942. FURI_LOG_E(TAG, "Failed to create pvp enemy context.");
  943. easy_flipper_dialog("Error", "Failed to create pvp enemy context. Press BACK to return.");
  944. flipper_http_free(fhttp);
  945. furi_string_free(lobby);
  946. return;
  947. }
  948. furi_string_free(lobby);
  949. // start the websocket session
  950. if (!game_start_ws(fhttp, lobby_list[lobby_index]))
  951. {
  952. FURI_LOG_E(TAG, "Failed to start websocket session");
  953. easy_flipper_dialog("Error", "Failed to start websocket session. Press BACK to return.");
  954. flipper_http_free(fhttp);
  955. return;
  956. }
  957. flipper_http_free(fhttp);
  958. // start the game thread
  959. if (!game_thread_start(app))
  960. {
  961. FURI_LOG_E(TAG, "Failed to start game thread");
  962. easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
  963. return;
  964. }
  965. };
  966. void game_waiting_process(FlipperHTTP *fhttp, void *context)
  967. {
  968. FlipWorldApp *app = (FlipWorldApp *)context;
  969. furi_check(app, "FlipWorldApp is NULL");
  970. if (!fhttp)
  971. {
  972. FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
  973. easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return.");
  974. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
  975. return;
  976. }
  977. // fetch the lobby details
  978. if (!game_fetch_lobby(fhttp, lobby_list[lobby_index]))
  979. {
  980. FURI_LOG_E(TAG, "Failed to fetch lobby details");
  981. flipper_http_free(fhttp);
  982. easy_flipper_dialog("Error", "Failed to fetch lobby details. Press BACK to return.");
  983. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
  984. return;
  985. }
  986. // load the lobby details
  987. FuriString *lobby = flipper_http_load_from_file(fhttp->file_path);
  988. if (!lobby)
  989. {
  990. FURI_LOG_E(TAG, "Failed to load lobby details");
  991. flipper_http_free(fhttp);
  992. easy_flipper_dialog("Error", "Failed to load lobby details. Press BACK to return.");
  993. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
  994. return;
  995. }
  996. // get the player count
  997. const size_t count = game_lobby_count(fhttp, lobby);
  998. if (count == 2)
  999. {
  1000. // break out of this and start the game
  1001. game_start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby
  1002. return;
  1003. }
  1004. furi_string_free(lobby);
  1005. }
  1006. void game_waiting_lobby(void *context)
  1007. {
  1008. FlipWorldApp *app = (FlipWorldApp *)context;
  1009. furi_check(app, "waiting_lobby: FlipWorldApp is NULL");
  1010. if (!game_start_waiting_thread(app))
  1011. {
  1012. FURI_LOG_E(TAG, "Failed to start waiting thread");
  1013. easy_flipper_dialog("Error", "Failed to start waiting thread. Press BACK to return.");
  1014. return;
  1015. }
  1016. free_message_view(app);
  1017. if (!alloc_message_view(app, MessageStateWaitingLobby))
  1018. {
  1019. FURI_LOG_E(TAG, "Failed to allocate message view");
  1020. return;
  1021. }
  1022. // finally, switch to the waiting lobby view
  1023. view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage);
  1024. };