Browse Source

final code cleanup (at least until 0.8.1)

jblanked 9 months ago
parent
commit
13bdcd184b
10 changed files with 1783 additions and 1271 deletions
  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);

File diff suppressed because it is too large
+ 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;
+};

Some files were not shown because too many files changed in this diff