Browse Source

final code cleanup (at least until 0.8.1)

jblanked 11 tháng trước cách đây
mục cha
commit
13bdcd184b
10 tập tin đã thay đổi với 1783 bổ sung1271 xóa
  1. 402 0
      callback/alloc.c
  2. 9 0
      callback/alloc.h
  3. 34 1218
      callback/callback.c
  4. 16 13
      callback/callback.h
  5. 31 30
      callback/free.c
  6. 1255 0
      callback/game.c
  7. 10 0
      callback/game.h
  8. 1 0
      callback/loader.c
  9. 0 10
      callback/loader.h
  10. 25 0
      callback/utils.h

+ 402 - 0
callback/alloc.c

@@ -0,0 +1,402 @@
+#include "callback/alloc.h"
+#include "alloc/alloc.h"
+#include "callback/callback.h"
+#include <flip_storage/storage.h>
+
+bool alloc_message_view(void *context, MessageState state)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->view_message)
+    {
+        FURI_LOG_E(TAG, "Message view already allocated");
+        return false;
+    }
+    switch (state)
+    {
+    case MessageStateAbout:
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app);
+        break;
+    case MessageStateLoading:
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, NULL, NULL, &app->view_dispatcher, app);
+        break;
+    case MessageStateWaitingLobby:
+        easy_flipper_set_view(&app->view_message, FlipWorldViewMessage, message_draw_callback, message_input_callback, NULL, &app->view_dispatcher, app);
+        break;
+    }
+    if (!app->view_message)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate message view");
+        return false;
+    }
+    view_allocate_model(app->view_message, ViewModelTypeLockFree, sizeof(MessageModel));
+    MessageModel *model = view_get_model(app->view_message);
+    model->message_state = state;
+    return true;
+}
+
+bool alloc_text_input_view(void *context, char *title)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (!title)
+    {
+        FURI_LOG_E(TAG, "Title is NULL");
+        return false;
+    }
+    app->text_input_buffer_size = 64;
+    if (!app->text_input_buffer)
+    {
+        if (!easy_flipper_set_buffer(&app->text_input_buffer, app->text_input_buffer_size))
+        {
+            return false;
+        }
+    }
+    if (!app->text_input_temp_buffer)
+    {
+        if (!easy_flipper_set_buffer(&app->text_input_temp_buffer, app->text_input_buffer_size))
+        {
+            return false;
+        }
+    }
+    if (!app->text_input)
+    {
+        if (!easy_flipper_set_uart_text_input(
+                &app->text_input,
+                FlipWorldViewTextInput,
+                title,
+                app->text_input_temp_buffer,
+                app->text_input_buffer_size,
+                is_str(title, "SSID") ? updated_wifi_ssid : is_str(title, "Password")     ? updated_wifi_pass
+                                                        : is_str(title, "Username-Login") ? updated_username
+                                                                                          : updated_password,
+                callback_to_wifi_settings,
+                &app->view_dispatcher,
+                app))
+        {
+            return false;
+        }
+        if (!app->text_input)
+        {
+            return false;
+        }
+        char ssid[64];
+        char pass[64];
+        char username[64];
+        char password[64];
+        if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password)))
+        {
+            if (is_str(title, "SSID"))
+            {
+                strncpy(app->text_input_temp_buffer, ssid, app->text_input_buffer_size);
+            }
+            else if (is_str(title, "Password"))
+            {
+                strncpy(app->text_input_temp_buffer, pass, app->text_input_buffer_size);
+            }
+            else if (is_str(title, "Username-Login"))
+            {
+                strncpy(app->text_input_temp_buffer, username, app->text_input_buffer_size);
+            }
+            else if (is_str(title, "Password-Login"))
+            {
+                strncpy(app->text_input_temp_buffer, password, app->text_input_buffer_size);
+            }
+        }
+    }
+    return true;
+}
+bool alloc_variable_item_list(void *context, uint32_t view_id)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    if (!app)
+    {
+        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
+        return false;
+    }
+    char ssid[64];
+    char pass[64];
+    char username[64];
+    char password[64];
+    if (!app->variable_item_list)
+    {
+        switch (view_id)
+        {
+        case FlipWorldSubmenuIndexWiFiSettings:
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, wifi_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate variable item list");
+                return false;
+            }
+
+            if (!app->variable_item_list)
+            {
+                FURI_LOG_E(TAG, "Variable item list is NULL");
+                return false;
+            }
+
+            if (!app->variable_item_wifi_ssid)
+            {
+                app->variable_item_wifi_ssid = variable_item_list_add(app->variable_item_list, "SSID", 0, NULL, NULL);
+                variable_item_set_current_value_text(app->variable_item_wifi_ssid, "");
+            }
+            if (!app->variable_item_wifi_pass)
+            {
+                app->variable_item_wifi_pass = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
+                variable_item_set_current_value_text(app->variable_item_wifi_pass, "");
+            }
+            if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password)))
+            {
+                variable_item_set_current_value_text(app->variable_item_wifi_ssid, ssid);
+                // variable_item_set_current_value_text(app->variable_item_wifi_pass, pass);
+                save_char("WiFi-SSID", ssid);
+                save_char("WiFi-Password", pass);
+                save_char("Flip-Social-Username", username);
+                save_char("Flip-Social-Password", password);
+            }
+            break;
+        case FlipWorldSubmenuIndexGameSettings:
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, game_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate variable item list");
+                return false;
+            }
+
+            if (!app->variable_item_list)
+            {
+                FURI_LOG_E(TAG, "Variable item list is NULL");
+                return false;
+            }
+
+            if (!app->variable_item_game_download_world)
+            {
+                app->variable_item_game_download_world = variable_item_list_add(app->variable_item_list, "Install Official World Pack", 0, NULL, NULL);
+                variable_item_set_current_value_text(app->variable_item_game_download_world, "");
+            }
+            if (!app->variable_item_game_player_sprite)
+            {
+                app->variable_item_game_player_sprite = variable_item_list_add(app->variable_item_list, "Weapon", 4, player_on_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_player_sprite, 1);
+                variable_item_set_current_value_text(app->variable_item_game_player_sprite, player_sprite_choices[1]);
+            }
+            if (!app->variable_item_game_fps)
+            {
+                app->variable_item_game_fps = variable_item_list_add(app->variable_item_list, "FPS", 4, fps_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_fps, 0);
+                variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[0]);
+            }
+            if (!app->variable_item_game_vgm_x)
+            {
+                app->variable_item_game_vgm_x = variable_item_list_add(app->variable_item_list, "VGM Horizontal", 12, vgm_x_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_vgm_x, 2);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[2]);
+            }
+            if (!app->variable_item_game_vgm_y)
+            {
+                app->variable_item_game_vgm_y = variable_item_list_add(app->variable_item_list, "VGM Vertical", 12, vgm_y_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_vgm_y, 2);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[2]);
+            }
+            if (!app->variable_item_game_screen_always_on)
+            {
+                app->variable_item_game_screen_always_on = variable_item_list_add(app->variable_item_list, "Keep Screen On?", 2, screen_on_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_screen_always_on, 1);
+                variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[1]);
+            }
+            if (!app->variable_item_game_sound_on)
+            {
+                app->variable_item_game_sound_on = variable_item_list_add(app->variable_item_list, "Sound On?", 2, sound_on_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_sound_on, 0);
+                variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[0]);
+            }
+            if (!app->variable_item_game_vibration_on)
+            {
+                app->variable_item_game_vibration_on = variable_item_list_add(app->variable_item_list, "Vibration On?", 2, vibration_on_change, NULL);
+                variable_item_set_current_value_index(app->variable_item_game_vibration_on, 0);
+                variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[0]);
+            }
+            char _game_player_sprite[8];
+            if (load_char("Game-Player-Sprite", _game_player_sprite, sizeof(_game_player_sprite)))
+            {
+                int index = is_str(_game_player_sprite, "naked") ? 0 : is_str(_game_player_sprite, "sword") ? 1
+                                                                   : is_str(_game_player_sprite, "axe")     ? 2
+                                                                   : is_str(_game_player_sprite, "bow")     ? 3
+                                                                                                            : 0;
+                variable_item_set_current_value_index(app->variable_item_game_player_sprite, index);
+                variable_item_set_current_value_text(
+                    app->variable_item_game_player_sprite,
+                    is_str(player_sprite_choices[index], "naked") ? "None" : player_sprite_choices[index]);
+            }
+            char _game_fps[8];
+            if (load_char("Game-FPS", _game_fps, sizeof(_game_fps)))
+            {
+                int index = is_str(_game_fps, "30") ? 0 : is_str(_game_fps, "60") ? 1
+                                                      : is_str(_game_fps, "120")  ? 2
+                                                      : is_str(_game_fps, "240")  ? 3
+                                                                                  : 0;
+                variable_item_set_current_value_text(app->variable_item_game_fps, fps_choices_str[index]);
+                variable_item_set_current_value_index(app->variable_item_game_fps, index);
+            }
+            char _game_vgm_x[8];
+            if (load_char("Game-VGM-X", _game_vgm_x, sizeof(_game_vgm_x)))
+            {
+                int vgm_x = atoi(_game_vgm_x);
+                int index = vgm_x == -2 ? 0 : vgm_x == -1 ? 1
+                                          : vgm_x == 0    ? 2
+                                          : vgm_x == 1    ? 3
+                                          : vgm_x == 2    ? 4
+                                          : vgm_x == 3    ? 5
+                                          : vgm_x == 4    ? 6
+                                          : vgm_x == 5    ? 7
+                                          : vgm_x == 6    ? 8
+                                          : vgm_x == 7    ? 9
+                                          : vgm_x == 8    ? 10
+                                          : vgm_x == 9    ? 11
+                                          : vgm_x == 10   ? 12
+                                                          : 2;
+                variable_item_set_current_value_index(app->variable_item_game_vgm_x, index);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_x, vgm_levels[index]);
+            }
+            char _game_vgm_y[8];
+            if (load_char("Game-VGM-Y", _game_vgm_y, sizeof(_game_vgm_y)))
+            {
+                int vgm_y = atoi(_game_vgm_y);
+                int index = vgm_y == -2 ? 0 : vgm_y == -1 ? 1
+                                          : vgm_y == 0    ? 2
+                                          : vgm_y == 1    ? 3
+                                          : vgm_y == 2    ? 4
+                                          : vgm_y == 3    ? 5
+                                          : vgm_y == 4    ? 6
+                                          : vgm_y == 5    ? 7
+                                          : vgm_y == 6    ? 8
+                                          : vgm_y == 7    ? 9
+                                          : vgm_y == 8    ? 10
+                                          : vgm_y == 9    ? 11
+                                          : vgm_y == 10   ? 12
+                                                          : 2;
+                variable_item_set_current_value_index(app->variable_item_game_vgm_y, index);
+                variable_item_set_current_value_text(app->variable_item_game_vgm_y, vgm_levels[index]);
+            }
+            char _game_screen_always_on[8];
+            if (load_char("Game-Screen-Always-On", _game_screen_always_on, sizeof(_game_screen_always_on)))
+            {
+                int index = is_str(_game_screen_always_on, "No") ? 0 : is_str(_game_screen_always_on, "Yes") ? 1
+                                                                                                             : 0;
+                variable_item_set_current_value_text(app->variable_item_game_screen_always_on, yes_or_no_choices[index]);
+                variable_item_set_current_value_index(app->variable_item_game_screen_always_on, index);
+            }
+            char _game_sound_on[8];
+            if (load_char("Game-Sound-On", _game_sound_on, sizeof(_game_sound_on)))
+            {
+                int index = is_str(_game_sound_on, "No") ? 0 : is_str(_game_sound_on, "Yes") ? 1
+                                                                                             : 0;
+                variable_item_set_current_value_text(app->variable_item_game_sound_on, yes_or_no_choices[index]);
+                variable_item_set_current_value_index(app->variable_item_game_sound_on, index);
+            }
+            char _game_vibration_on[8];
+            if (load_char("Game-Vibration-On", _game_vibration_on, sizeof(_game_vibration_on)))
+            {
+                int index = is_str(_game_vibration_on, "No") ? 0 : is_str(_game_vibration_on, "Yes") ? 1
+                                                                                                     : 0;
+                variable_item_set_current_value_text(app->variable_item_game_vibration_on, yes_or_no_choices[index]);
+                variable_item_set_current_value_index(app->variable_item_game_vibration_on, index);
+            }
+            break;
+        case FlipWorldSubmenuIndexUserSettings:
+            if (!easy_flipper_set_variable_item_list(&app->variable_item_list, FlipWorldViewVariableItemList, user_settings_select, callback_to_settings, &app->view_dispatcher, app))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate variable item list");
+                return false;
+            }
+
+            if (!app->variable_item_list)
+            {
+                FURI_LOG_E(TAG, "Variable item list is NULL");
+                return false;
+            }
+
+            // if logged in, show profile info, otherwise show login/register
+            if (is_logged_in() || is_logged_in_to_flip_social())
+            {
+                if (!app->variable_item_user_username)
+                {
+                    app->variable_item_user_username = variable_item_list_add(app->variable_item_list, "Username", 0, NULL, NULL);
+                    variable_item_set_current_value_text(app->variable_item_user_username, "");
+                }
+                if (!app->variable_item_user_password)
+                {
+                    app->variable_item_user_password = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
+                    variable_item_set_current_value_text(app->variable_item_user_password, "");
+                }
+                if (load_settings(ssid, sizeof(ssid), pass, sizeof(pass), username, sizeof(username), password, sizeof(password)))
+                {
+                    variable_item_set_current_value_text(app->variable_item_user_username, username);
+                    variable_item_set_current_value_text(app->variable_item_user_password, "*****");
+                }
+            }
+            else
+            {
+                if (!app->variable_item_user_username)
+                {
+                    app->variable_item_user_username = variable_item_list_add(app->variable_item_list, "Username", 0, NULL, NULL);
+                    variable_item_set_current_value_text(app->variable_item_user_username, "");
+                }
+                if (!app->variable_item_user_password)
+                {
+                    app->variable_item_user_password = variable_item_list_add(app->variable_item_list, "Password", 0, NULL, NULL);
+                    variable_item_set_current_value_text(app->variable_item_user_password, "");
+                }
+            }
+            break;
+        }
+    }
+    return true;
+}
+bool alloc_submenu_other(void *context, uint32_t view_id)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->submenu_other)
+    {
+        FURI_LOG_I(TAG, "Submenu already allocated");
+        return true;
+    }
+    switch (view_id)
+    {
+    case FlipWorldViewSettings:
+        if (!easy_flipper_set_submenu(&app->submenu_other, FlipWorldViewSubmenuOther, "Settings", callback_to_submenu, &app->view_dispatcher))
+        {
+            FURI_LOG_E(TAG, "Failed to allocate submenu settings");
+            return false;
+        }
+        submenu_add_item(app->submenu_other, "WiFi", FlipWorldSubmenuIndexWiFiSettings, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_other, "Game", FlipWorldSubmenuIndexGameSettings, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_other, "User", FlipWorldSubmenuIndexUserSettings, callback_submenu_choices, app);
+        return true;
+    case FlipWorldViewLobby:
+        return easy_flipper_set_submenu(&app->submenu_other, FlipWorldViewSubmenuOther, "Lobbies", callback_to_submenu, &app->view_dispatcher);
+    default:
+        return false;
+    }
+}
+
+bool alloc_game_submenu(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (!app->submenu_game)
+    {
+        if (!easy_flipper_set_submenu(&app->submenu_game, FlipWorldViewGameSubmenu, "Play", callback_to_submenu, &app->view_dispatcher))
+        {
+            return false;
+        }
+        if (!app->submenu_game)
+        {
+            return false;
+        }
+        submenu_add_item(app->submenu_game, "Tutorial", FlipWorldSubmenuIndexStory, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_game, "PvP (Beta)", FlipWorldSubmenuIndexPvP, callback_submenu_choices, app);
+        submenu_add_item(app->submenu_game, "PvE", FlipWorldSubmenuIndexPvE, callback_submenu_choices, app);
+    }
+    return true;
+}

+ 9 - 0
callback/alloc.h

@@ -0,0 +1,9 @@
+#pragma once
+#include <flip_world.h>
+#include <callback/utils.h>
+
+bool alloc_message_view(void *context, MessageState state);
+bool alloc_text_input_view(void *context, char *title);
+bool alloc_variable_item_list(void *context, uint32_t view_id);
+bool alloc_submenu_other(void *context, uint32_t view_id);
+bool alloc_game_submenu(void *context);

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 34 - 1218
callback/callback.c


+ 16 - 13
callback/callback.h

@@ -2,16 +2,19 @@
 #include <flip_world.h>
 
 void callback_submenu_choices(void *context, uint32_t index);
-
-typedef enum MessageState MessageState;
-enum MessageState
-{
-    MessageStateAbout,        // The about screen
-    MessageStateLoading,      // The loading screen (for game)
-    MessageStateWaitingLobby, // The waiting lobby screen
-};
-typedef struct MessageModel MessageModel;
-struct MessageModel
-{
-    MessageState message_state;
-};
+bool message_input_callback(InputEvent *event, void *context);
+void message_draw_callback(Canvas *canvas, void *model);
+void wifi_settings_select(void *context, uint32_t index);
+void updated_wifi_ssid(void *context);
+void updated_wifi_pass(void *context);
+void updated_username(void *context);
+void updated_password(void *context);
+void fps_change(VariableItem *item);
+void game_settings_select(void *context, uint32_t index);
+void user_settings_select(void *context, uint32_t index);
+void screen_on_change(VariableItem *item);
+void sound_on_change(VariableItem *item);
+void vibration_on_change(VariableItem *item);
+void player_on_change(VariableItem *item);
+void vgm_x_change(VariableItem *item);
+void vgm_y_change(VariableItem *item);

+ 31 - 30
callback/free.c

@@ -1,5 +1,6 @@
 #include "callback/free.h"
 #include "callback/loader.h"
+#include "callback/game.h"
 
 void free_game_submenu(void *context)
 {
@@ -23,14 +24,14 @@ void free_submenu_other(void *context)
         submenu_free(app->submenu_other);
         app->submenu_other = NULL;
     }
-    // for (int i = 0; i < 10; i++)
-    // {
-    //     if (lobby_list[i])
-    //     {
-    //         free(lobby_list[i]);
-    //         lobby_list[i] = NULL;
-    //     }
-    // }
+    for (int i = 0; i < 10; i++)
+    {
+        if (lobby_list[i])
+        {
+            free(lobby_list[i]);
+            lobby_list[i] = NULL;
+        }
+    }
 }
 
 void free_message_view(void *context)
@@ -151,17 +152,17 @@ void free_all_views(void *context, bool free_variable_list, bool free_settings_o
     free_text_input_view(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);
-    //         game_thread = NULL;
-    //     }
-    // }
+    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);
+            game_thread = NULL;
+        }
+    }
 
     if (free_settings_other)
     {
@@ -177,15 +178,15 @@ void free_all_views(void *context, bool free_variable_list, bool free_settings_o
     }
 
     // free waiting 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);
-    //         waiting_thread = NULL;
-    //     }
-    // }
+    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);
+            waiting_thread = NULL;
+        }
+    }
 }

+ 1255 - 0
callback/game.c

@@ -0,0 +1,1255 @@
+#include <callback/game.h>
+#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 <callback/loader.h>
+#include <callback/free.h>
+#include <callback/alloc.h>
+#include "alloc/alloc.h"
+#include <flip_storage/storage.h>
+
+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
+    }
+}

+ 10 - 0
callback/game.h

@@ -0,0 +1,10 @@
+#pragma once
+#include <flip_world.h>
+extern bool user_hit_back;
+extern uint32_t lobby_index;
+extern char *lobby_list[10];
+extern FuriThread *game_thread;
+extern FuriThread *waiting_thread;
+extern bool game_thread_running;
+extern bool waiting_thread_running;
+void run(FlipWorldApp *app);

+ 1 - 0
callback/loader.c

@@ -1,4 +1,5 @@
 #include <callback/loader.h>
+#include <callback/utils.h>
 #include <alloc/alloc.h>
 
 bool loader_view_alloc(void *context)

+ 0 - 10
callback/loader.h

@@ -1,16 +1,6 @@
 #pragma once
 #include <flip_world.h>
 
-// Below added by Derek Jamison
-// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
-#ifdef DEVELOPMENT
-#define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
-#define DEV_CRASH() furi_crash()
-#else
-#define FURI_LOG_DEV(tag, format, ...)
-#define DEV_CRASH()
-#endif
-
 typedef enum DataState DataState;
 enum DataState
 {

+ 25 - 0
callback/utils.h

@@ -0,0 +1,25 @@
+#pragma once
+#include <flip_world.h>
+
+// Below added by Derek Jamison
+// FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
+#ifdef DEVELOPMENT
+#define FURI_LOG_DEV(tag, format, ...) furi_log_print_format(FuriLogLevelInfo, tag, format, ##__VA_ARGS__)
+#define DEV_CRASH() furi_crash()
+#else
+#define FURI_LOG_DEV(tag, format, ...)
+#define DEV_CRASH()
+#endif
+
+typedef enum MessageState MessageState;
+enum MessageState
+{
+    MessageStateAbout,        // The about screen
+    MessageStateLoading,      // The loading screen (for game)
+    MessageStateWaitingLobby, // The waiting lobby screen
+};
+typedef struct MessageModel MessageModel;
+struct MessageModel
+{
+    MessageState message_state;
+};

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác