#include #include "engine/engine.h" #include "engine/game_engine.h" #include "engine/game_manager_i.h" #include "engine/level_i.h" #include "engine/entity_i.h" #include "game/storage.h" #include #include #include #include "alloc/alloc.h" #include bool user_hit_back = false; uint32_t lobby_index = -1; char *lobby_list[10]; static uint8_t timer_iteration = 0; // timer iteration for the loading screen static uint8_t timer_refresh = 5; // duration for timer to refresh // static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context); static void waiting_lobby(void *context); static bool fetch_lobby(FlipperHTTP *fhttp, char *lobby_name); // FuriThread *game_thread = NULL; FuriThread *waiting_thread = NULL; bool game_thread_running = false; bool waiting_thread_running = false; // static void callback_submenu_lobby_choices(void *context, uint32_t index); static void frame_cb(GameEngine *engine, Canvas *canvas, InputState input, void *context) { UNUSED(engine); GameManager *game_manager = context; game_manager_input_set(game_manager, input); game_manager_update(game_manager); game_manager_render(game_manager, canvas); } static int32_t game_app(void *p) { UNUSED(p); GameManager *game_manager = game_manager_alloc(); if (!game_manager) { FURI_LOG_E("Game", "Failed to allocate game manager"); return -1; } // Setup game engine settings... GameEngineSettings settings = game_engine_settings_init(); settings.target_fps = atof_(fps_choices_str[fps_index]); settings.show_fps = game.show_fps; settings.always_backlight = strstr(yes_or_no_choices[screen_always_on_index], "Yes") != NULL; settings.frame_callback = frame_cb; settings.context = game_manager; GameEngine *engine = game_engine_alloc(settings); if (!engine) { FURI_LOG_E("Game", "Failed to allocate game engine"); game_manager_free(game_manager); return -1; } game_manager_engine_set(game_manager, engine); // Allocate custom game context if needed void *game_context = NULL; if (game.context_size > 0) { game_context = malloc(game.context_size); game_manager_game_context_set(game_manager, game_context); } // Start the game game.start(game_manager, game_context); // 1) Run the engine game_engine_run(engine); // 2) Stop the game FIRST, so it can do any internal cleanup game.stop(game_context); // 3) Now free the engine game_engine_free(engine); // 4) Now free the manager game_manager_free(game_manager); // 5) Finally, free your custom context if it was allocated if (game_context) { free(game_context); } // 6) Check for leftover entities int32_t entities = entities_get_count(); if (entities != 0) { FURI_LOG_E("Game", "Memory leak detected: %ld entities still allocated", entities); return -1; } return 0; } static int32_t waiting_app_callback(void *p) { FlipWorldApp *app = (FlipWorldApp *)p; furi_check(app); FlipperHTTP *fhttp = flipper_http_alloc(); if (!fhttp) { FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP"); return -1; } user_hit_back = false; timer_iteration = 0; while (timer_iteration < 60 && !user_hit_back) { FURI_LOG_I(TAG, "Waiting for more players..."); waiting_loader_process_callback(fhttp, app); FURI_LOG_I(TAG, "Waiting for more players... %d", timer_iteration); timer_iteration++; furi_delay_ms(1000 * timer_refresh); } // if we reach here, it means we timed out or the user hit back FURI_LOG_E(TAG, "No players joined within the timeout or user hit back"); remove_player_from_lobby(fhttp); flipper_http_free(fhttp); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther); return 0; } static bool start_waiting_thread(void *context) { FlipWorldApp *app = (FlipWorldApp *)context; furi_check(app); // free game thread if (waiting_thread_running) { waiting_thread_running = false; if (waiting_thread) { furi_thread_flags_set(furi_thread_get_id(waiting_thread), WorkerEvtStop); furi_thread_join(waiting_thread); furi_thread_free(waiting_thread); } } // start waiting thread FuriThread *thread = furi_thread_alloc_ex("waiting_thread", 2048, waiting_app_callback, app); if (!thread) { FURI_LOG_E(TAG, "Failed to allocate waiting thread"); easy_flipper_dialog("Error", "Failed to allocate waiting thread. Restart your Flipper."); return false; } furi_thread_start(thread); waiting_thread = thread; waiting_thread_running = true; return true; } static bool fetch_world_list(FlipperHTTP *fhttp) { if (!fhttp) { FURI_LOG_E(TAG, "fhttp is NULL"); easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); return false; } // ensure flip_world directory exists char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds"); storage_common_mkdir(storage, directory_path); furi_record_close(RECORD_STORAGE); snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); fhttp->save_received_data = true; return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/v5/list/10/", "{\"Content-Type\":\"application/json\"}", NULL); } // we will load the palyer stats from the API and save them // in player_spawn game method, it will load the player stats that we saved static bool fetch_player_stats(FlipperHTTP *fhttp) { if (!fhttp) { FURI_LOG_E(TAG, "fhttp is NULL"); easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); return false; } char username[64]; if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update."); return false; } char url[128]; snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/user/game-stats/%s/", username); // ensure the folders exist char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data"); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player"); storage_common_mkdir(storage, directory_path); furi_record_close(RECORD_STORAGE); snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json"); fhttp->save_received_data = true; return flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL); } // static bool fetch_app_update(FlipperHTTP *fhttp) // { // if (!fhttp) // { // FURI_LOG_E(TAG, "fhttp is NULL"); // easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); // return false; // } // return flipper_http_get_request_with_headers(fhttp, "https://www.jblanked.com/flipper/api/app/last-updated/flip_world/", "{\"Content-Type\":\"application/json\"}"); // } // static bool parse_app_update(FlipperHTTP *fhttp) // { // if (!fhttp) // { // FURI_LOG_E(TAG, "fhttp is NULL"); // easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return."); // return false; // } // if (fhttp->last_response == NULL || strlen(fhttp->last_response) == 0) // { // FURI_LOG_E(TAG, "fhttp->last_response is NULL or empty"); // easy_flipper_dialog("Error", "fhttp->last_response is NULL or empty. Press BACK to return."); // return false; // } // bool last_update_available = false; // char last_updated_old[32]; // // load the previous last_updated // if (!load_char("last_updated", last_updated_old, sizeof(last_updated_old))) // { // FURI_LOG_E(TAG, "Failed to load last_updated"); // // it's okay, we'll just update it // } // // save the new last_updated // save_char("last_updated", fhttp->last_response); // // compare the two // if (strlen(last_updated_old) == 0 || !is_str(last_updated_old, fhttp->last_response)) // { // last_update_available = true; // } // if (last_update_available) // { // easy_flipper_dialog("Update Available", "An update is available. Press OK to update."); // return true; // } // else // { // easy_flipper_dialog("No Update Available", "No update is available. Press OK to continue."); // return false; // } // } static bool start_game_thread(void *context) { FlipWorldApp *app = (FlipWorldApp *)context; if (!app) { FURI_LOG_E(TAG, "app is NULL"); easy_flipper_dialog("Error", "app is NULL. Press BACK to return."); return false; } // free everything but message_view free_variable_item_list(app); free_text_input_view(app); // free_submenu_other(app); // free lobby list or settings loader_view_free(app); free_game_submenu(app); // free game thread if (game_thread_running) { game_thread_running = false; if (game_thread) { furi_thread_flags_set(furi_thread_get_id(game_thread), WorkerEvtStop); furi_thread_join(game_thread); furi_thread_free(game_thread); } } // start game thread FuriThread *thread = furi_thread_alloc_ex("game", 2048, game_app, app); if (!thread) { FURI_LOG_E(TAG, "Failed to allocate game thread"); easy_flipper_dialog("Error", "Failed to allocate game thread. Restart your Flipper."); return false; } furi_thread_start(thread); game_thread = thread; game_thread_running = true; return true; } // combine register, login, and world list fetch into one function to switch to the loader view static bool _fetch_game(DataLoaderModel *model) { FlipWorldApp *app = (FlipWorldApp *)model->parser_context; if (!app) { FURI_LOG_E(TAG, "app is NULL"); easy_flipper_dialog("Error", "app is NULL. Press BACK to return."); return false; } if (model->request_index == 0) { // login char username[64]; char password[64]; if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now easy_flipper_dialog("Error", "Failed to load saved username\nGo to user settings to update."); return false; } if (!load_char("Flip-Social-Password", password, sizeof(password))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now easy_flipper_dialog("Error", "Failed to load saved password\nGo to settings to update."); return false; } char payload[256]; snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password); return flipper_http_request(model->fhttp, POST, "https://www.jblanked.com/flipper/api/user/login/", "{\"Content-Type\":\"application/json\"}", payload); } else if (model->request_index == 1) { // check if login was successful char is_logged_in[8]; if (!load_char("is_logged_in", is_logged_in, sizeof(is_logged_in))) { FURI_LOG_E(TAG, "Failed to load is_logged_in"); easy_flipper_dialog("Error", "Failed to load is_logged_in\nGo to user settings to update."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return false; } if (is_str(is_logged_in, "false") && is_str(model->title, "Registering...")) { // register char username[64]; char password[64]; if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return false; } if (!load_char("Flip-Social-Password", password, sizeof(password))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); easy_flipper_dialog("Error", "Failed to load saved password. Go to settings to update."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return false; } char payload[172]; snprintf(payload, sizeof(payload), "{\"username\":\"%s\",\"password\":\"%s\"}", username, password); model->title = "Registering..."; return flipper_http_request(model->fhttp, POST, "https://www.jblanked.com/flipper/api/user/register/", "{\"Content-Type\":\"application/json\"}", payload); } else { model->title = "Fetching World List.."; return fetch_world_list(model->fhttp); } } else if (model->request_index == 2) { model->title = "Fetching World List.."; return fetch_world_list(model->fhttp); } else if (model->request_index == 3) { snprintf(model->fhttp->file_path, sizeof(model->fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json"); FuriString *world_list = flipper_http_load_from_file(model->fhttp->file_path); if (!world_list) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now FURI_LOG_E(TAG, "Failed to load world list"); easy_flipper_dialog("Error", "Failed to load world list. Go to game settings to download packs."); return false; } FuriString *first_world = get_json_array_value_furi("worlds", 0, world_list); if (!first_world) { view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now FURI_LOG_E(TAG, "Failed to get first world"); easy_flipper_dialog("Error", "Failed to get first world. Go to game settings to download packs."); furi_string_free(world_list); return false; } if (world_exists(furi_string_get_cstr(first_world))) { furi_string_free(world_list); furi_string_free(first_world); if (!start_game_thread(app)) { FURI_LOG_E(TAG, "Failed to start game thread"); easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Failed to start game thread"; } return true; } 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)); model->fhttp->save_received_data = true; char url[128]; snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/v5/get/world/%s/", furi_string_get_cstr(first_world)); furi_string_free(world_list); furi_string_free(first_world); return flipper_http_request(model->fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL); } FURI_LOG_E(TAG, "Unknown request index"); return false; } static char *_parse_game(DataLoaderModel *model) { FlipWorldApp *app = (FlipWorldApp *)model->parser_context; if (model->request_index == 0) { if (!model->fhttp->last_response) { save_char("is_logged_in", "false"); // Go back to the main menu easy_flipper_dialog("Error", "Response is empty. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Response is empty..."; } // Check for successful conditions if (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User found") != NULL) { save_char("is_logged_in", "true"); model->title = "Login successful!"; model->title = "Fetching World List.."; return "Login successful!"; } // Check if user not found if (strstr(model->fhttp->last_response, "User not found") != NULL) { save_char("is_logged_in", "false"); model->title = "Registering..."; return "Account not found...\nRegistering now.."; // if they see this an issue happened switching to register } // If not success, not found, check length conditions size_t resp_len = strlen(model->fhttp->last_response); if (resp_len == 0 || resp_len > 127) { // Empty or too long means failed login save_char("is_logged_in", "false"); // Go back to the main menu easy_flipper_dialog("Error", "Failed to login. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to login..."; } // Handle any other unknown response as a failure save_char("is_logged_in", "false"); // Go back to the main menu easy_flipper_dialog("Error", "Failed to login. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to login..."; } else if (model->request_index == 1) { if (is_str(model->title, "Registering...")) { // check registration response if (model->fhttp->last_response != NULL && (strstr(model->fhttp->last_response, "[SUCCESS]") != NULL || strstr(model->fhttp->last_response, "User created") != NULL)) { save_char("is_logged_in", "true"); char username[64]; char password[64]; // load the username and password, then save them if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); easy_flipper_dialog("Error", "Failed to load Flip-Social-Username"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to load Flip-Social-Username"; } if (!load_char("Flip-Social-Password", password, sizeof(password))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Password"); easy_flipper_dialog("Error", "Failed to load Flip-Social-Password"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to load Flip-Social-Password"; } // load wifi ssid,pass then save char ssid[64]; char pass[64]; if (!load_char("WiFi-SSID", ssid, sizeof(ssid))) { FURI_LOG_E(TAG, "Failed to load WiFi-SSID"); easy_flipper_dialog("Error", "Failed to load WiFi-SSID"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to load WiFi-SSID"; } if (!load_char("WiFi-Password", pass, sizeof(pass))) { FURI_LOG_E(TAG, "Failed to load WiFi-Password"); easy_flipper_dialog("Error", "Failed to load WiFi-Password"); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); return "Failed to load WiFi-Password"; } save_settings(ssid, pass, username, password); model->title = "Fetching World List.."; return "Account created!"; } else if (strstr(model->fhttp->last_response, "Username or password not provided") != NULL) { easy_flipper_dialog("Error", "Please enter your credentials.\nPress BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Please enter your credentials."; } else if (strstr(model->fhttp->last_response, "User already exists") != NULL || strstr(model->fhttp->last_response, "Multiple users found") != NULL) { easy_flipper_dialog("Error", "Registration failed...\nUsername already exists.\nPress BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Username already exists."; } else { easy_flipper_dialog("Error", "Registration failed...\nUpdate your credentials.\nPress BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Registration failed..."; } } else { if (!start_game_thread(app)) { FURI_LOG_E(TAG, "Failed to start game thread"); easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Failed to start game thread"; } return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; } } else if (model->request_index == 2) { return "Welcome to FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; } else if (model->request_index == 3) { if (!start_game_thread(app)) { FURI_LOG_E(TAG, "Failed to start game thread"); easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Failed to start game thread"; } return "Thanks for playing FlipWorld!\n\n\n\nPress BACK to return if this\ndoesn't automatically close."; } easy_flipper_dialog("Error", "Unknown error. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now return "Unknown error"; } static void switch_to_view_get_game(FlipWorldApp *app) { if (!loader_view_alloc(app)) { FURI_LOG_E(TAG, "Failed to allocate view loader"); return; } loader_switch_to_view(app, "Starting Game..", _fetch_game, _parse_game, 5, callback_to_submenu, FlipWorldViewLoader); } void run(FlipWorldApp *app) { if (!app) { FURI_LOG_E(TAG, "FlipWorldApp is NULL"); return; } free_all_views(app, true, true, false); // only need to check if they have 50k free (game needs about 38k currently) if (!is_enough_heap(50000, false)) { const size_t min_free = memmgr_get_free_heap(); char message[64]; snprintf(message, sizeof(message), "Not enough heap memory.\nThere are %zu bytes free.", min_free); easy_flipper_dialog("Error", message); return; } // check if logged in if (is_logged_in() || is_logged_in_to_flip_social()) { FlipperHTTP *fhttp = flipper_http_alloc(); if (!fhttp) { FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return."); return; } bool fetch_world_list_i() { return fetch_world_list(fhttp); } bool parse_world_list_i() { return fhttp->state != ISSUE; } bool fetch_player_stats_i() { return fetch_player_stats(fhttp); } if (!alloc_message_view(app, MessageStateLoading)) { FURI_LOG_E(TAG, "Failed to allocate message view"); return; } view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage); // Make the request if (game_mode_index != 1) // not GAME_MODE_PVP { if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context)) { FURI_LOG_E(HTTP_TAG, "Failed to make request"); flipper_http_free(fhttp); } else { flipper_http_free(fhttp); } if (!start_game_thread(app)) { FURI_LOG_E(TAG, "Failed to start game thread"); easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); return; } } else { // load pvp info (this returns the lobbies available) bool fetch_pvp_lobbies() { // ensure flip_world directory exists char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp"); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies"); storage_common_mkdir(storage, directory_path); furi_record_close(RECORD_STORAGE); snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/pvp_lobbies.json"); storage_simply_remove_recursive(storage, fhttp->file_path); // ensure the file is empty fhttp->save_received_data = true; // 2 players max, 10 lobbies return flipper_http_request(fhttp, GET, "https://www.jblanked.com/flipper/api/world/pvp/lobbies/2/10/", "{\"Content-Type\":\"application/json\"}", NULL); } bool parse_pvp_lobbies() { free_submenu_other(app); if (!alloc_submenu_other(app, FlipWorldViewLobby)) { FURI_LOG_E(TAG, "Failed to allocate lobby submenu"); return false; } // add the lobbies to the submenu FuriString *lobbies = flipper_http_load_from_file(fhttp->file_path); if (!lobbies) { FURI_LOG_E(TAG, "Failed to load lobbies"); return false; } // parse the lobbies for (uint32_t i = 0; i < 10; i++) { FuriString *lobby = get_json_array_value_furi("lobbies", i, lobbies); if (!lobby) { FURI_LOG_I(TAG, "No more lobbies"); break; } FuriString *lobby_id = get_json_value_furi("id", lobby); if (!lobby_id) { FURI_LOG_E(TAG, "Failed to get lobby id"); furi_string_free(lobby); return false; } // add the lobby to the submenu submenu_add_item(app->submenu_other, furi_string_get_cstr(lobby_id), FlipWorldSubmenuIndexLobby + i, callback_submenu_lobby_choices, app); // add the lobby to the list if (!easy_flipper_set_buffer(&lobby_list[i], 64)) { FURI_LOG_E(TAG, "Failed to allocate lobby list"); furi_string_free(lobby); furi_string_free(lobby_id); return false; } snprintf(lobby_list[i], 64, "%s", furi_string_get_cstr(lobby_id)); furi_string_free(lobby); furi_string_free(lobby_id); } furi_string_free(lobbies); return true; } // load pvp lobbies and player stats if (!flipper_http_process_response_async(fhttp, fetch_pvp_lobbies, parse_pvp_lobbies) || !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context)) { // unlike the pve/story, receiving data is necessary // so send the user back to the main menu if it fails FURI_LOG_E(HTTP_TAG, "Failed to make request"); easy_flipper_dialog("Error", "Failed to make request. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); flipper_http_free(fhttp); } else { flipper_http_free(fhttp); } // switch to the lobby submenu view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther); } } else { switch_to_view_get_game(app); } } static bool fetch_lobby(FlipperHTTP *fhttp, char *lobby_name) { if (!fhttp) { FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return false; } if (!lobby_name || strlen(lobby_name) == 0) { FURI_LOG_E(TAG, "Lobby name is NULL or empty"); return false; } char username[64]; if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); return false; } // send the request to fetch the lobby details, with player_username char url[128]; snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/pvp/lobby/get/%s/%s/", lobby_name, username); snprintf(fhttp->file_path, sizeof(fhttp->file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/pvp/lobbies/%s.json", lobby_name); fhttp->save_received_data = true; if (!flipper_http_request(fhttp, GET, url, "{\"Content-Type\":\"application/json\"}", NULL)) { FURI_LOG_E(TAG, "Failed to fetch lobby details"); return false; } fhttp->state = RECEIVING; while (fhttp->state != IDLE) { furi_delay_ms(100); } return true; } static bool join_lobby(FlipperHTTP *fhttp, char *lobby_name) { if (!fhttp) { FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return false; } if (!lobby_name || strlen(lobby_name) == 0) { FURI_LOG_E(TAG, "Lobby name is NULL or empty"); return false; } char username[64]; if (!load_char("Flip-Social-Username", username, sizeof(username))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); return false; } char url[128]; char payload[128]; snprintf(payload, sizeof(payload), "{\"username\":\"%s\", \"game_id\":\"%s\"}", username, lobby_name); save_char("pvp_lobby_name", lobby_name); // save the lobby name snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/pvp/lobby/join/"); if (!flipper_http_request(fhttp, POST, url, "{\"Content-Type\":\"application/json\"}", payload)) { FURI_LOG_E(TAG, "Failed to join lobby"); return false; } fhttp->state = RECEIVING; while (fhttp->state != IDLE) { furi_delay_ms(100); } return true; } static bool create_pvp_enemy(FuriString *lobby_details) { if (!lobby_details) { FURI_LOG_E(TAG, "Failed to load lobby details"); return false; } char current_user[64]; if (!load_char("Flip-Social-Username", current_user, sizeof(current_user))) { FURI_LOG_E(TAG, "Failed to load Flip-Social-Username"); save_char("create_pvp_error", "Failed to load Flip-Social-Username"); return false; } for (uint8_t i = 0; i < 2; i++) { // parse the lobby details FuriString *player_stats = get_json_array_value_furi("player_stats", i, lobby_details); if (!player_stats) { FURI_LOG_E(TAG, "Failed to get player stats"); save_char("create_pvp_error", "Failed to get player stats array"); return false; } // available keys from player_stats FuriString *username = get_json_value_furi("username", player_stats); if (!username) { FURI_LOG_E(TAG, "Failed to get username"); save_char("create_pvp_error", "Failed to get username"); furi_string_free(player_stats); return false; } // check if the username is the same as the current user if (is_str(furi_string_get_cstr(username), current_user)) { furi_string_free(player_stats); furi_string_free(username); continue; // skip the current user } FuriString *strength = get_json_value_furi("strength", player_stats); FuriString *health = get_json_value_furi("health", player_stats); FuriString *attack_timer = get_json_value_furi("attack_timer", player_stats); if (!strength || !health || !attack_timer) { FURI_LOG_E(TAG, "Failed to get player stats"); save_char("create_pvp_error", "Failed to get player stats"); furi_string_free(player_stats); furi_string_free(username); if (strength) furi_string_free(strength); if (health) furi_string_free(health); if (attack_timer) furi_string_free(attack_timer); return false; } // create enemy data FuriString *enemy_data = furi_string_alloc(); furi_string_printf( enemy_data, "{\"enemy_data\":[{\"id\":\"sword\",\"is_user\":\"true\",\"username\":\"%s\"," "\"index\":0,\"start_position\":{\"x\":350,\"y\":210},\"end_position\":{\"x\":350,\"y\":210}," "\"move_timer\":1,\"speed\":1,\"attack_timer\":%f,\"strength\":%f,\"health\":%f}]}", furi_string_get_cstr(username), (double)atof_furi(attack_timer), (double)atof_furi(strength), (double)atof_furi(health)); char directory_path[128]; snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world"); Storage *storage = furi_record_open(RECORD_STORAGE); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds"); storage_common_mkdir(storage, directory_path); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/pvp_world"); storage_common_mkdir(storage, directory_path); furi_record_close(RECORD_STORAGE); snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/pvp_world/pvp_world_enemy_data.json"); // remove the enemy_data file if it exists storage_simply_remove_recursive(storage, directory_path); File *file = storage_file_alloc(storage); if (!storage_file_open(file, directory_path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) { FURI_LOG_E("Game", "Failed to open file for writing: %s", directory_path); save_char("create_pvp_error", "Failed to open file for writing"); storage_file_free(file); furi_record_close(RECORD_STORAGE); furi_string_free(enemy_data); furi_string_free(player_stats); furi_string_free(username); furi_string_free(strength); furi_string_free(health); furi_string_free(attack_timer); return false; } size_t data_size = furi_string_size(enemy_data); if (storage_file_write(file, furi_string_get_cstr(enemy_data), data_size) != data_size) { FURI_LOG_E("Game", "Failed to write enemy_data"); save_char("create_pvp_error", "Failed to write enemy_data"); } storage_file_close(file); furi_string_free(enemy_data); furi_string_free(player_stats); furi_string_free(username); furi_string_free(strength); furi_string_free(health); furi_string_free(attack_timer); // player is found so break break; } return true; } // since we aren't using FURI_LOG, we will use easy_flipper_dialog and the last_error_message // char last_error_message[64]; static size_t lobby_count(FlipperHTTP *fhttp, FuriString *lobby) { if (!fhttp) { FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return -1; } if (!lobby) { FURI_LOG_E(TAG, "Lobby details are NULL"); return -1; } // check if the player is in the lobby FuriString *player_count = get_json_value_furi("player_count", lobby); if (!player_count) { FURI_LOG_E(TAG, "Failed to get player count"); return -1; } const size_t count = atoi(furi_string_get_cstr(player_count)); furi_string_free(player_count); return count; } static bool in_lobby(FlipperHTTP *fhttp, FuriString *lobby) { if (!fhttp) { FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return false; } if (!lobby) { FURI_LOG_E(TAG, "Lobby details are NULL"); return false; } // check if the player is in the lobby FuriString *is_in_game = get_json_value_furi("is_in_game", lobby); if (!is_in_game) { FURI_LOG_E(TAG, "Failed to get is_in_game"); furi_string_free(is_in_game); return false; } const bool in_game = is_str(furi_string_get_cstr(is_in_game), "true"); furi_string_free(is_in_game); return in_game; } static bool start_ws(FlipperHTTP *fhttp, char *lobby_name) { if (!fhttp) { FURI_LOG_E(TAG, "FlipperHTTP is NULL"); return false; } if (!lobby_name || strlen(lobby_name) == 0) { FURI_LOG_E(TAG, "Lobby name is NULL or empty"); return false; } fhttp->state = IDLE; // ensure it's set to IDLE for the next request char websocket_url[128]; snprintf(websocket_url, sizeof(websocket_url), "ws://www.jblanked.com/ws/game/%s/", lobby_name); if (!flipper_http_websocket_start(fhttp, websocket_url, 80, "{\"Content-Type\":\"application/json\"}")) { FURI_LOG_E(TAG, "Failed to start websocket"); return false; } fhttp->state = RECEIVING; while (fhttp->state != IDLE) { furi_delay_ms(100); } return true; } // this will free both the fhttp and lobby static void start_pvp(FlipperHTTP *fhttp, FuriString *lobby, void *context) { FlipWorldApp *app = (FlipWorldApp *)context; furi_check(app, "FlipWorldApp is NULL"); // only thing left to do is create the enemy data and start the websocket session if (!create_pvp_enemy(lobby)) { FURI_LOG_E(TAG, "Failed to create pvp enemy context."); easy_flipper_dialog("Error", "Failed to create pvp enemy context. Press BACK to return."); flipper_http_free(fhttp); furi_string_free(lobby); return; } furi_string_free(lobby); // start the websocket session if (!start_ws(fhttp, lobby_list[lobby_index])) { FURI_LOG_E(TAG, "Failed to start websocket session"); easy_flipper_dialog("Error", "Failed to start websocket session. Press BACK to return."); flipper_http_free(fhttp); return; } flipper_http_free(fhttp); // start the game thread if (!start_game_thread(app)) { FURI_LOG_E(TAG, "Failed to start game thread"); easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return."); return; } }; static void waiting_loader_process_callback(FlipperHTTP *fhttp, void *context) { FlipWorldApp *app = (FlipWorldApp *)context; if (!app) { FURI_LOG_E(TAG, "FlipWorldApp is NULL"); return; } if (!fhttp) { FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther); return; } // fetch the lobby details if (!fetch_lobby(fhttp, lobby_list[lobby_index])) { FURI_LOG_E(TAG, "Failed to fetch lobby details"); flipper_http_free(fhttp); easy_flipper_dialog("Error", "Failed to fetch lobby details. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther); return; } // load the lobby details FuriString *lobby = flipper_http_load_from_file(fhttp->file_path); if (!lobby) { FURI_LOG_E(TAG, "Failed to load lobby details"); flipper_http_free(fhttp); easy_flipper_dialog("Error", "Failed to load lobby details. Press BACK to return."); view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenuOther); return; } // get the player count const size_t count = lobby_count(fhttp, lobby); if (count == 2) { // break out of this and start the game start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby return; } furi_string_free(lobby); } static void waiting_lobby(void *context) { FlipWorldApp *app = (FlipWorldApp *)context; furi_check(app, "waiting_lobby: FlipWorldApp is NULL"); if (!start_waiting_thread(app)) { FURI_LOG_E(TAG, "Failed to start waiting thread"); easy_flipper_dialog("Error", "Failed to start waiting thread. Press BACK to return."); return; } free_message_view(app); if (!alloc_message_view(app, MessageStateWaitingLobby)) { FURI_LOG_E(TAG, "Failed to allocate message view"); return; } // finally, switch to the waiting lobby view view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewMessage); }; static void callback_submenu_lobby_choices(void *context, uint32_t index) { /* Handle other game lobbies 1. when clicked on, send request to fetch the selected game lobby details 2. start the websocket session 3. start the game thread (the rest will be handled by game_start and player_update) */ FlipWorldApp *app = (FlipWorldApp *)context; furi_check(app, "FlipWorldApp is NULL"); if (index >= FlipWorldSubmenuIndexLobby && index < FlipWorldSubmenuIndexLobby + 10) { lobby_index = index - FlipWorldSubmenuIndexLobby; FlipperHTTP *fhttp = flipper_http_alloc(); if (!fhttp) { FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP"); easy_flipper_dialog("Error", "Failed to allocate FlipperHTTP. Press BACK to return."); return; } // fetch the lobby details if (!fetch_lobby(fhttp, lobby_list[lobby_index])) { FURI_LOG_E(TAG, "Failed to fetch lobby details"); easy_flipper_dialog("Error", "Failed to fetch lobby details. Press BACK to return."); flipper_http_free(fhttp); return; } // load the lobby details FuriString *lobby = flipper_http_load_from_file(fhttp->file_path); if (!lobby) { FURI_LOG_E(TAG, "Failed to load lobby details"); flipper_http_free(fhttp); return; } // if there are no players, add the user to the lobby and make the user wait until another player joins // if there is one player and it's the user, make the user wait until another player joins // if there is one player and it's not the user, parse_lobby and start websocket // if there are 2 players (which there shouldn't be at this point), show an error message saying the lobby is full switch (lobby_count(fhttp, lobby)) { case -1: FURI_LOG_E(TAG, "Failed to get player count"); easy_flipper_dialog("Error", "Failed to get player count. Press BACK to return."); flipper_http_free(fhttp); furi_string_free(lobby); return; case 0: // add the user to the lobby if (!join_lobby(fhttp, lobby_list[lobby_index])) { FURI_LOG_E(TAG, "Failed to join lobby"); easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return."); flipper_http_free(fhttp); furi_string_free(lobby); return; } // send the user to the waiting screen waiting_lobby(app); return; case 1: // check if the user is in the lobby if (in_lobby(fhttp, lobby)) { // send the user to the waiting screen FURI_LOG_I(TAG, "User is in the lobby"); flipper_http_free(fhttp); furi_string_free(lobby); waiting_lobby(app); return; } // add the user to the lobby if (!join_lobby(fhttp, lobby_list[lobby_index])) { FURI_LOG_E(TAG, "Failed to join lobby"); easy_flipper_dialog("Error", "Failed to join lobby. Press BACK to return."); flipper_http_free(fhttp); furi_string_free(lobby); return; } break; case 2: // show an error message saying the lobby is full FURI_LOG_E(TAG, "Lobby is full"); easy_flipper_dialog("Error", "Lobby is full. Press BACK to return."); flipper_http_free(fhttp); furi_string_free(lobby); return; }; start_pvp(fhttp, lobby, app); // this will free both the fhttp and lobby, and start the game } }