Просмотр исходного кода

Merge pull request #21 from jblanked/dev_0.8

FlipWorld - v0.8
- Improved memory allocation.
- Added multiplayer support.
- Updated the default vibration/sound settings.
JBlanked 9 месяцев назад
Родитель
Сommit
6d8864e47e

+ 9 - 3
README.md

@@ -1,8 +1,14 @@
 # FlipWorld
 # FlipWorld
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 
 
+## Connect Online
+- Discord: https://discord.gg/5aN9qwkEc6
+- YouTube: https://www.youtube.com/@jblanked
+- Instagram: https://www.instagram.com/jblanked
+- Other: https://www.jblanked.com/social/
+
 ## Requirements
 ## Requirements
-- WiFi Developer Board, Raspberry Pi, or ESP32 device with the [FlipperHTTP flash](https://github.com/jblanked/FlipperHTTP).
+- WiFi Developer Board, Raspberry Pi, or ESP32 device flashed with [FlipperHTTP v1.8.2](https://github.com/jblanked/FlipperHTTP) or higher.
 - 2.4 GHz WiFi access point
 - 2.4 GHz WiFi access point
 
 
 ## How It Works
 ## How It Works
@@ -21,7 +27,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold DOWN**: Walk down.
 - **Press/Hold DOWN**: Walk down.
-- **Press OK**: Interact/Attack/Teleport (set to attack until all enemies are defeated and interact when colliding with NPCs)
+- **Press OK**: Interact, attack, or teleport. Attacks enemies when colliding with them until all enemies are defeated. Interacts with NPCs when colliding with them.
 - **HOLD OK**: In-Game Menu.
 - **HOLD OK**: In-Game Menu.
 - **Press BACK**: Leave the menu.
 - **Press BACK**: Leave the menu.
 - **HOLD BACK**: Exit the game.
 - **HOLD BACK**: Exit the game.
@@ -85,7 +91,7 @@ NPCs are friendly characters that players can interact with. Currently, you can
 - New game features
 - New game features
 
 
 **v0.8**
 **v0.8**
-- Multiplayer support
+- Multiplayer support (PvP Beta)
 
 
 **v1.0**
 **v1.0**
 - Official release
 - Official release

+ 25 - 52
alloc/alloc.c

@@ -1,15 +1,29 @@
 #include <alloc/alloc.h>
 #include <alloc/alloc.h>
 #include <callback/callback.h>
 #include <callback/callback.h>
+#include <callback/loader.h>
+#include <callback/free.h>
 
 
-/**
- * @brief Navigation callback for exiting the application
- * @param context The context - unused
- * @return next view id (VIEW_NONE to exit the app)
- */
-static uint32_t callback_exit_app(void *context)
+uint32_t callback_exit_app(void *context)
 {
 {
     UNUSED(context);
     UNUSED(context);
-    return VIEW_NONE; // Return VIEW_NONE to exit the app
+    return VIEW_NONE;
+}
+
+uint32_t callback_to_submenu(void *context)
+{
+    UNUSED(context);
+    return FlipWorldViewSubmenu;
+}
+
+uint32_t callback_to_wifi_settings(void *context)
+{
+    UNUSED(context);
+    return FlipWorldViewVariableItemList;
+}
+uint32_t callback_to_settings(void *context)
+{
+    UNUSED(context);
+    return FlipWorldViewSubmenuOther;
 }
 }
 
 
 void *global_app;
 void *global_app;
@@ -35,34 +49,16 @@ FlipWorldApp *flip_world_app_alloc()
     {
     {
         return NULL;
         return NULL;
     }
     }
-    view_dispatcher_set_custom_event_callback(app->view_dispatcher, custom_event_callback);
-    // Main view
-    if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
-    {
-        return NULL;
-    }
-    loader_init(app->view_loader);
-    if (!easy_flipper_set_widget(&app->widget_result, FlipWorldViewWidgetResult, "", callback_to_submenu, &app->view_dispatcher))
-    {
-        return NULL;
-    }
 
 
     // Submenu
     // Submenu
     if (!easy_flipper_set_submenu(&app->submenu, FlipWorldViewSubmenu, VERSION_TAG, callback_exit_app, &app->view_dispatcher))
     if (!easy_flipper_set_submenu(&app->submenu, FlipWorldViewSubmenu, VERSION_TAG, callback_exit_app, &app->view_dispatcher))
     {
     {
         return NULL;
         return NULL;
     }
     }
-    if (!easy_flipper_set_submenu(&app->submenu_game, FlipWorldViewGameSubmenu, "Play", callback_to_submenu, &app->view_dispatcher))
-    {
-        return NULL;
-    }
+
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexGameSubmenu, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexGameSubmenu, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexMessage, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexMessage, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Settings", FlipWorldSubmenuIndexSettings, callback_submenu_choices, app);
-    //
-    submenu_add_item(app->submenu_game, "Tutorial", FlipWorldSubmenuIndexStory, callback_submenu_choices, app);
-    submenu_add_item(app->submenu_game, "PvP", FlipWorldSubmenuIndexPvP, callback_submenu_choices, app);
-    submenu_add_item(app->submenu_game, "PvE", FlipWorldSubmenuIndexPvE, callback_submenu_choices, app);
 
 
     // Switch to the main view
     // Switch to the main view
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
     view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
@@ -73,11 +69,7 @@ FlipWorldApp *flip_world_app_alloc()
 // Function to free the resources used by FlipWorldApp
 // Function to free the resources used by FlipWorldApp
 void flip_world_app_free(FlipWorldApp *app)
 void flip_world_app_free(FlipWorldApp *app)
 {
 {
-    if (!app)
-    {
-        FURI_LOG_E(TAG, "FlipWorldApp is NULL");
-        return;
-    }
+    furi_check(app, "FlipWorldApp is NULL");
 
 
     // Free Submenu(s)
     // Free Submenu(s)
     if (app->submenu)
     if (app->submenu)
@@ -85,27 +77,8 @@ void flip_world_app_free(FlipWorldApp *app)
         view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSubmenu);
         view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSubmenu);
         submenu_free(app->submenu);
         submenu_free(app->submenu);
     }
     }
-    if (app->submenu_game)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewGameSubmenu);
-        submenu_free(app->submenu_game);
-    }
-    // Free Widget(s)
-    if (app->widget_result)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewWidgetResult);
-        widget_free(app->widget_result);
-    }
 
 
-    // Free View(s)
-    if (app->view_loader)
-    {
-        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewLoader);
-        loader_free_model(app->view_loader);
-        view_free(app->view_loader);
-    }
-
-    free_all_views(app, true, true);
+    free_all_views(app, true, true, true);
 
 
     // free the view dispatcher
     // free the view dispatcher
     view_dispatcher_free(app->view_dispatcher);
     view_dispatcher_free(app->view_dispatcher);
@@ -116,4 +89,4 @@ void flip_world_app_free(FlipWorldApp *app)
     // free the app
     // free the app
     if (app)
     if (app)
         free(app);
         free(app);
-}
+}

+ 4 - 1
alloc/alloc.h

@@ -1,6 +1,9 @@
 #pragma once
 #pragma once
 #include <flip_world.h>
 #include <flip_world.h>
-
 FlipWorldApp *flip_world_app_alloc();
 FlipWorldApp *flip_world_app_alloc();
 void flip_world_app_free(FlipWorldApp *app);
 void flip_world_app_free(FlipWorldApp *app);
 void flip_world_show_submenu();
 void flip_world_show_submenu();
+uint32_t callback_exit_app(void *context);
+uint32_t callback_to_submenu(void *context);
+uint32_t callback_to_wifi_settings(void *context);
+uint32_t callback_to_settings(void *context);

+ 7 - 0
app.c

@@ -4,6 +4,13 @@
 // Entry point for the FlipWorld application
 // Entry point for the FlipWorld application
 int32_t flip_world_main(void *p)
 int32_t flip_world_main(void *p)
 {
 {
+    // check memory
+    if (!is_enough_heap(sizeof(FlipWorldApp) + sizeof(FlipperHTTP), true))
+    {
+        easy_flipper_dialog("Memory Error", "Not enough heap memory.\nPlease restart your Flipper Zero.");
+        return 0; // return success so the user can see the error
+    }
+
     // Suppress unused parameter warning
     // Suppress unused parameter warning
     UNUSED(p);
     UNUSED(p);
 
 

+ 1 - 1
application.fam

@@ -17,5 +17,5 @@ App(
     ),
     ),
     fap_author="JBlanked",
     fap_author="JBlanked",
     fap_weburl="https://github.com/jblanked/FlipWorld",
     fap_weburl="https://github.com/jblanked/FlipWorld",
-    fap_version="0.7",
+    fap_version="0.8",
 )
 )

BIN
assets/01-home.png


+ 5 - 0
assets/CHANGELOG.md

@@ -1,3 +1,8 @@
+## 0.8 (2025-04-05)
+- Improved memory allocation.
+- Added multiplayer support.
+- Updated the default vibration/sound settings.
+
 ## 0.7 (2025-03-21)
 ## 0.7 (2025-03-21)
 - Sped up player movement.
 - Sped up player movement.
 - Added a Tutorial mode.
 - Added a Tutorial mode.

+ 10 - 4
assets/README.md

@@ -1,7 +1,13 @@
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 The first open-world multiplayer game for the Flipper Zero, best played with the VGM. Here's a video tutorial: https://www.youtube.com/watch?v=Qp7qmYMfdUA
 
 
+## Connect Online
+- Discord: https://discord.gg/5aN9qwkEc6
+- YouTube: https://www.youtube.com/@jblanked
+- Instagram: https://www.instagram.com/jblanked
+- Other: https://www.jblanked.com/social/
+
 ## Requirements
 ## Requirements
-- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
+- WiFi Developer Board, Raspberry Pi, or ESP32 device flashed with FlipperHTTP v1.8.2 or higher: https://github.com/jblanked/FlipperHTTP
 - 2.4 GHz WiFi access point
 - 2.4 GHz WiFi access point
 
 
 ## How It Works
 ## How It Works
@@ -20,7 +26,7 @@ FlipWorld and FlipSocial are connected. Your login information is the same in bo
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold RIGHT**: Turn right if not already facing right, then walk right if the button is still pressed.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold UP**: Walk up.
 - **Press/Hold DOWN**: Walk down.
 - **Press/Hold DOWN**: Walk down.
-- **Press OK**: Interact/Attack/Teleport (set to attack until all enemies are defeated and interact when colliding with NPCs)
+- **Press OK**: Interact, attack, or teleport. Attacks enemies when colliding with them until all enemies are defeated. Interacts with NPCs when colliding with them.
 - **HOLD OK**: In-Game Menu.
 - **HOLD OK**: In-Game Menu.
 - **Press BACK**: Leave the menu.
 - **Press BACK**: Leave the menu.
 - **HOLD BACK**: Exit the game.
 - **HOLD BACK**: Exit the game.
@@ -57,7 +63,7 @@ NPCs are friendly characters that players can interact with. Currently, you can
 3. Restart your Flipper Zero, then open FlipWorld.
 3. Restart your Flipper Zero, then open FlipWorld.
 4. Click "Settings -> WiFi", then input your WiFi SSID and password.
 4. Click "Settings -> WiFi", then input your WiFi SSID and password.
 5. Hit the "BACK" button, click "User". If your username is not present, click "Username" and add one. Do the same for the password field.
 5. Hit the "BACK" button, click "User". If your username is not present, click "Username" and add one. Do the same for the password field.
-6. Go back to the main menu and hit "Play". It will register an account if necessary and fetch data from our API that's used to render our graphics.
+6. Go back to the main menu and hit "Play", followed by "Tutorial". It will register an account if necessary and fetch data from our API that's used to render our graphics.
 
 
 ## Roadmap
 ## Roadmap
 
 
@@ -84,7 +90,7 @@ NPCs are friendly characters that players can interact with. Currently, you can
 - New game features
 - New game features
 
 
 **v0.8**
 **v0.8**
-- Multiplayer support
+- Multiplayer support (PvP Beta)
 
 
 **v1.0**
 **v1.0**
 - Official release
 - Official release

BIN
assets/icon_alien_gun_10x10px.png


BIN
assets/icon_axe_10x10px.png


BIN
assets/icon_axe_16x16px.png


BIN
assets/icon_axe_alt_10x10px.png


BIN
assets/icon_axe_alt_16x16px.png


BIN
assets/icon_bow_10x10px.png


BIN
assets/icon_title_screen_128x64px.png


+ 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);

Разница между файлами не показана из-за своего большого размера
+ 47 - 1232
callback/callback.c


+ 16 - 53
callback/callback.h

@@ -1,57 +1,20 @@
 #pragma once
 #pragma once
 #include <flip_world.h>
 #include <flip_world.h>
-#include <flip_storage/storage.h>
 
 
-void free_all_views(void *context, bool should_free_variable_item_list, bool should_free_submenu_settings);
 void callback_submenu_choices(void *context, uint32_t index);
 void callback_submenu_choices(void *context, uint32_t index);
-uint32_t callback_to_submenu(void *context);
-
-// Add edits by Derek Jamison
-typedef enum DataState DataState;
-enum DataState
-{
-    DataStateInitial,
-    DataStateRequested,
-    DataStateReceived,
-    DataStateParsed,
-    DataStateParseError,
-    DataStateError,
-};
-
-typedef enum MessageState MessageState;
-enum MessageState
-{
-    MessageStateAbout,
-    MessageStateLoading,
-};
-typedef struct MessageModel MessageModel;
-struct MessageModel
-{
-    MessageState message_state;
-};
-
-typedef struct DataLoaderModel DataLoaderModel;
-typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
-typedef char *(*DataLoaderParser)(DataLoaderModel *model);
-struct DataLoaderModel
-{
-    char *title;
-    char *data_text;
-    DataState data_state;
-    DataLoaderFetch fetcher;
-    DataLoaderParser parser;
-    void *parser_context;
-    size_t request_index;
-    size_t request_count;
-    ViewNavigationCallback back_callback;
-    FuriTimer *timer;
-    FlipperHTTP *fhttp;
-};
-void generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
-
-void loader_draw_callback(Canvas *canvas, void *model);
-
-void loader_init(View *view);
-
-void loader_free_model(View *view);
-bool custom_event_callback(void *context, uint32_t index);
+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);

+ 192 - 0
callback/free.c

@@ -0,0 +1,192 @@
+#include "callback/free.h"
+#include "callback/loader.h"
+#include "callback/game.h"
+
+void free_game_submenu(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->submenu_game)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewGameSubmenu);
+        submenu_free(app->submenu_game);
+        app->submenu_game = NULL;
+    }
+}
+
+void free_submenu_other(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->submenu_other)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewSubmenuOther);
+        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;
+        }
+    }
+}
+
+void free_message_view(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->view_message)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewMessage);
+        view_free(app->view_message);
+        app->view_message = NULL;
+    }
+}
+
+void free_text_input_view(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->text_input)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewTextInput);
+        uart_text_input_free(app->text_input);
+        app->text_input = NULL;
+    }
+    if (app->text_input_buffer)
+    {
+        free(app->text_input_buffer);
+        app->text_input_buffer = NULL;
+    }
+    if (app->text_input_temp_buffer)
+    {
+        free(app->text_input_temp_buffer);
+        app->text_input_temp_buffer = NULL;
+    }
+}
+
+void free_variable_item_list(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (app->variable_item_list)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewVariableItemList);
+        variable_item_list_free(app->variable_item_list);
+        app->variable_item_list = NULL;
+    }
+    if (app->variable_item_wifi_ssid)
+    {
+        free(app->variable_item_wifi_ssid);
+        app->variable_item_wifi_ssid = NULL;
+    }
+    if (app->variable_item_wifi_pass)
+    {
+        free(app->variable_item_wifi_pass);
+        app->variable_item_wifi_pass = NULL;
+    }
+    if (app->variable_item_game_fps)
+    {
+        free(app->variable_item_game_fps);
+        app->variable_item_game_fps = NULL;
+    }
+    if (app->variable_item_game_screen_always_on)
+    {
+        free(app->variable_item_game_screen_always_on);
+        app->variable_item_game_screen_always_on = NULL;
+    }
+    if (app->variable_item_game_download_world)
+    {
+        free(app->variable_item_game_download_world);
+        app->variable_item_game_download_world = NULL;
+    }
+    if (app->variable_item_game_sound_on)
+    {
+        free(app->variable_item_game_sound_on);
+        app->variable_item_game_sound_on = NULL;
+    }
+    if (app->variable_item_game_vibration_on)
+    {
+        free(app->variable_item_game_vibration_on);
+        app->variable_item_game_vibration_on = NULL;
+    }
+    if (app->variable_item_game_player_sprite)
+    {
+        free(app->variable_item_game_player_sprite);
+        app->variable_item_game_player_sprite = NULL;
+    }
+    if (app->variable_item_game_vgm_x)
+    {
+        free(app->variable_item_game_vgm_x);
+        app->variable_item_game_vgm_x = NULL;
+    }
+    if (app->variable_item_game_vgm_y)
+    {
+        free(app->variable_item_game_vgm_y);
+        app->variable_item_game_vgm_y = NULL;
+    }
+    if (app->variable_item_user_username)
+    {
+        free(app->variable_item_user_username);
+        app->variable_item_user_username = NULL;
+    }
+    if (app->variable_item_user_password)
+    {
+        free(app->variable_item_user_password);
+        app->variable_item_user_password = NULL;
+    }
+}
+
+void free_all_views(void *context, bool free_variable_list, bool free_settings_other, bool free_submenu_game)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app);
+    if (free_variable_list)
+    {
+        free_variable_item_list(app);
+    }
+    free_message_view(app);
+    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 (free_settings_other)
+    {
+        free_submenu_other(app);
+    }
+
+    // free Derek's loader
+    loader_view_free(app);
+
+    if (free_submenu_game)
+    {
+        free_game_submenu(app);
+    }
+
+    // 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;
+        }
+    }
+}

+ 9 - 0
callback/free.h

@@ -0,0 +1,9 @@
+#pragma once
+#include <flip_world.h>
+
+void free_game_submenu(void *context);
+void free_submenu_other(void *context);
+void free_message_view(void *context);
+void free_text_input_view(void *context);
+void free_variable_item_list(void *context);
+void free_all_views(void *context, bool free_variable_list, bool free_settings_other, bool free_submenu_game);

+ 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);

+ 590 - 0
callback/loader.c

@@ -0,0 +1,590 @@
+#include <callback/loader.h>
+#include <callback/utils.h>
+#include <alloc/alloc.h>
+
+bool loader_view_alloc(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app, "FlipWorldApp is NULL");
+    if (app->view_loader)
+    {
+        FURI_LOG_E(TAG, "View loader already allocated");
+        return false;
+    }
+    if (app->widget_result)
+    {
+        FURI_LOG_E(TAG, "Widget result already allocated");
+        return false;
+    }
+
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, loader_custom_event_callback);
+
+    if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        return false;
+    }
+
+    loader_init(app->view_loader);
+
+    return easy_flipper_set_widget(&app->widget_result, FlipWorldViewWidgetResult, "", callback_to_submenu, &app->view_dispatcher);
+}
+
+void loader_view_free(void *context)
+{
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    furi_check(app, "FlipWorldApp is NULL");
+    // Free Widget(s)
+    if (app->widget_result)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewWidgetResult);
+        widget_free(app->widget_result);
+        app->widget_result = NULL;
+    }
+
+    // Free View(s)
+    if (app->view_loader)
+    {
+        view_dispatcher_remove_view(app->view_dispatcher, FlipWorldViewLoader);
+        loader_free_model(app->view_loader);
+        view_free(app->view_loader);
+        app->view_loader = NULL;
+    }
+}
+
+static void loader_error_draw(Canvas *canvas, DataLoaderModel *model)
+{
+    if (canvas == NULL)
+    {
+        FURI_LOG_E(TAG, "error_draw - canvas is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if (model->fhttp->last_response != NULL)
+    {
+        if (strstr(model->fhttp->last_response, "[ERROR] Not connected to Wifi. Failed to reconnect.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[ERROR] Failed to connect to Wifi.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Not connected to Wifi.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[ERROR] GET request failed or returned empty data.") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] WiFi error.");
+            canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+            canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+        }
+        else if (strstr(model->fhttp->last_response, "[PONG]") != NULL)
+        {
+            canvas_clear(canvas);
+            canvas_draw_str(canvas, 0, 10, "[STATUS]Connecting to AP...");
+        }
+        else
+        {
+            canvas_clear(canvas);
+            FURI_LOG_E(TAG, "Received an error: %s", model->fhttp->last_response);
+            canvas_draw_str(canvas, 0, 10, "[ERROR] Unusual error...");
+            canvas_draw_str(canvas, 0, 60, "Press BACK and retry.");
+        }
+    }
+    else
+    {
+        canvas_clear(canvas);
+        canvas_draw_str(canvas, 0, 10, "[ERROR] Unknown error.");
+        canvas_draw_str(canvas, 0, 50, "Update your WiFi settings.");
+        canvas_draw_str(canvas, 0, 60, "Press BACK to return.");
+    }
+}
+
+static void loader_process_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_process_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    View *view = app->view_loader;
+
+    DataState current_data_state;
+    DataLoaderModel *loader_model = NULL;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            current_data_state = model->data_state;
+            loader_model = model;
+        },
+        false);
+    if (!loader_model || !loader_model->fhttp)
+    {
+        FURI_LOG_E(TAG, "Model or fhttp is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    if (current_data_state == DataStateInitial)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                model->data_state = DataStateRequested;
+                DataLoaderFetch fetch = model->fetcher;
+                if (fetch == NULL)
+                {
+                    FURI_LOG_E(TAG, "Model doesn't have Fetch function assigned.");
+                    model->data_state = DataStateError;
+                    return;
+                }
+
+                // Clear any previous responses
+                strncpy(model->fhttp->last_response, "", 1);
+                bool request_status = fetch(model);
+                if (!request_status)
+                {
+                    model->data_state = DataStateError;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateRequested || current_data_state == DataStateError)
+    {
+        if (loader_model->fhttp->state == IDLE && loader_model->fhttp->last_response != NULL)
+        {
+            if (strstr(loader_model->fhttp->last_response, "[PONG]") != NULL)
+            {
+                FURI_LOG_DEV(TAG, "PONG received.");
+            }
+            else if (strncmp(loader_model->fhttp->last_response, "[SUCCESS]", 9))
+            {
+                FURI_LOG_DEV(TAG, "SUCCESS received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            }
+            else if (strncmp(loader_model->fhttp->last_response, "[ERROR]", 9))
+            {
+                FURI_LOG_DEV(TAG, "ERROR received. %s", loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            }
+            else if (strlen(loader_model->fhttp->last_response))
+            {
+                // Still waiting on response
+            }
+            else
+            {
+                with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateReceived; }, true);
+            }
+        }
+        else if (loader_model->fhttp->state == SENDING || loader_model->fhttp->state == RECEIVING)
+        {
+            // continue waiting
+        }
+        else if (loader_model->fhttp->state == INACTIVE)
+        {
+            // inactive. try again
+        }
+        else if (loader_model->fhttp->state == ISSUE)
+        {
+            with_view_model(view, DataLoaderModel * model, { model->data_state = DataStateError; }, true);
+        }
+        else
+        {
+            FURI_LOG_DEV(TAG, "Unexpected state: %d lastresp: %s", loader_model->fhttp->state, loader_model->fhttp->last_response ? loader_model->fhttp->last_response : "NULL");
+            DEV_CRASH();
+        }
+    }
+    else if (current_data_state == DataStateReceived)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                char *data_text;
+                if (model->parser == NULL)
+                {
+                    data_text = NULL;
+                    FURI_LOG_DEV(TAG, "Parser is NULL");
+                    DEV_CRASH();
+                }
+                else
+                {
+                    data_text = model->parser(model);
+                }
+                FURI_LOG_DEV(TAG, "Parsed data: %s\r\ntext: %s", model->fhttp->last_response ? model->fhttp->last_response : "NULL", data_text ? data_text : "NULL");
+                model->data_text = data_text;
+                if (data_text == NULL)
+                {
+                    model->data_state = DataStateParseError;
+                }
+                else
+                {
+                    model->data_state = DataStateParsed;
+                }
+            },
+            true);
+    }
+    else if (current_data_state == DataStateParsed)
+    {
+        with_view_model(
+            view,
+            DataLoaderModel * model,
+            {
+                if (++model->request_index < model->request_count)
+                {
+                    model->data_state = DataStateInitial;
+                }
+                else
+                {
+                    loader_widget_set_text(model->data_text != NULL ? model->data_text : "", &app->widget_result);
+                    if (model->data_text != NULL)
+                    {
+                        free(model->data_text);
+                        model->data_text = NULL;
+                    }
+                    view_set_previous_callback(widget_get_view(app->widget_result), model->back_callback);
+                    view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewWidgetResult);
+                }
+            },
+            true);
+    }
+}
+
+bool loader_custom_event_callback(void *context, uint32_t index)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "custom_event_callback - context is NULL");
+        DEV_CRASH();
+        return false;
+    }
+
+    switch (index)
+    {
+    case FlipWorldCustomEventProcess:
+        loader_process_callback(context);
+        return true;
+    default:
+        FURI_LOG_DEV(TAG, "custom_event_callback. Unknown index: %ld", index);
+        return false;
+    }
+}
+
+void loader_draw_callback(Canvas *canvas, void *model)
+{
+    if (!canvas || !model)
+    {
+        FURI_LOG_E(TAG, "loader_draw_callback - canvas or model is NULL");
+        return;
+    }
+
+    DataLoaderModel *data_loader_model = (DataLoaderModel *)model;
+    HTTPState http_state = data_loader_model->fhttp->state;
+    DataState data_state = data_loader_model->data_state;
+    char *title = data_loader_model->title;
+
+    canvas_set_font(canvas, FontSecondary);
+
+    if (http_state == INACTIVE)
+    {
+        canvas_draw_str(canvas, 0, 7, "Wifi Dev Board disconnected.");
+        canvas_draw_str(canvas, 0, 17, "Please connect to the board.");
+        canvas_draw_str(canvas, 0, 32, "If your board is connected,");
+        canvas_draw_str(canvas, 0, 42, "make sure you have flashed");
+        canvas_draw_str(canvas, 0, 52, "your WiFi Devboard with the");
+        canvas_draw_str(canvas, 0, 62, "latest FlipperHTTP flash.");
+        return;
+    }
+
+    if (data_state == DataStateError || data_state == DataStateParseError)
+    {
+        loader_error_draw(canvas, data_loader_model);
+        return;
+    }
+
+    canvas_draw_str(canvas, 0, 7, title);
+    canvas_draw_str(canvas, 0, 17, "Loading...");
+
+    if (data_state == DataStateInitial)
+    {
+        return;
+    }
+
+    if (http_state == SENDING)
+    {
+        canvas_draw_str(canvas, 0, 27, "Fetching...");
+        return;
+    }
+
+    if (http_state == RECEIVING || data_state == DataStateRequested)
+    {
+        canvas_draw_str(canvas, 0, 27, "Receiving...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateReceived)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processing...");
+        return;
+    }
+
+    if (http_state == IDLE && data_state == DataStateParsed)
+    {
+        canvas_draw_str(canvas, 0, 27, "Processed...");
+        return;
+    }
+}
+
+static void loader_timer_callback(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_timer_callback - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, FlipWorldCustomEventProcess);
+}
+
+static void loader_on_enter(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_on_enter - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            view_set_previous_callback(view, model->back_callback);
+            if (model->timer == NULL)
+            {
+                model->timer = furi_timer_alloc(loader_timer_callback, FuriTimerTypePeriodic, app);
+            }
+            furi_timer_start(model->timer, 250);
+        },
+        true);
+}
+
+static void loader_on_exit(void *context)
+{
+    if (context == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_on_exit - context is NULL");
+        DEV_CRASH();
+        return;
+    }
+    FlipWorldApp *app = (FlipWorldApp *)context;
+    View *view = app->view_loader;
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_stop(model->timer);
+            }
+        },
+        false);
+}
+
+void loader_init(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_init - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    view_allocate_model(view, ViewModelTypeLocking, sizeof(DataLoaderModel));
+    view_set_enter_callback(view, loader_on_enter);
+    view_set_exit_callback(view, loader_on_exit);
+}
+
+void loader_free_model(View *view)
+{
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_free_model - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            if (model->timer)
+            {
+                furi_timer_free(model->timer);
+                model->timer = NULL;
+            }
+            if (model->parser_context)
+            {
+                // do not free the context here, it is the app context
+                // free(model->parser_context);
+                // model->parser_context = NULL;
+            }
+            if (model->fhttp)
+            {
+                flipper_http_free(model->fhttp);
+                model->fhttp = NULL;
+            }
+        },
+        false);
+}
+
+void loader_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id)
+{
+    if (app == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_switch_to_view - app is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    View *view = app->view_loader;
+    if (view == NULL)
+    {
+        FURI_LOG_E(TAG, "loader_switch_to_view - view is NULL");
+        DEV_CRASH();
+        return;
+    }
+
+    with_view_model(
+        view,
+        DataLoaderModel * model,
+        {
+            model->title = title;
+            model->fetcher = fetcher;
+            model->parser = parser;
+            model->request_index = 0;
+            model->request_count = request_count;
+            model->back_callback = back;
+            model->data_state = DataStateInitial;
+            model->data_text = NULL;
+            //
+            model->parser_context = app;
+            if (!model->fhttp)
+            {
+                model->fhttp = flipper_http_alloc();
+            }
+        },
+        true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, view_id);
+}
+
+void loader_widget_set_text(char *message, Widget **widget)
+{
+    if (widget == NULL)
+    {
+        FURI_LOG_E(TAG, "set_widget_text - widget is NULL");
+        DEV_CRASH();
+        return;
+    }
+    if (message == NULL)
+    {
+        FURI_LOG_E(TAG, "set_widget_text - message is NULL");
+        DEV_CRASH();
+        return;
+    }
+    widget_reset(*widget);
+
+    uint32_t message_length = strlen(message); // Length of the message
+    uint32_t i = 0;                            // Index tracker
+    uint32_t formatted_index = 0;              // Tracker for where we are in the formatted message
+    char *formatted_message;                   // Buffer to hold the final formatted message
+
+    // Allocate buffer with double the message length plus one for safety
+    if (!easy_flipper_set_buffer(&formatted_message, message_length * 2 + 1))
+    {
+        return;
+    }
+
+    while (i < message_length)
+    {
+        uint32_t max_line_length = 31;                  // Maximum characters per line
+        uint32_t remaining_length = message_length - i; // Remaining characters
+        uint32_t line_length = (remaining_length < max_line_length) ? remaining_length : max_line_length;
+
+        // Check for newline character within the current segment
+        uint32_t newline_pos = i;
+        bool found_newline = false;
+        for (; newline_pos < i + line_length && newline_pos < message_length; newline_pos++)
+        {
+            if (message[newline_pos] == '\n')
+            {
+                found_newline = true;
+                break;
+            }
+        }
+
+        if (found_newline)
+        {
+            // If newline found, set line_length up to the newline
+            line_length = newline_pos - i;
+        }
+
+        // Temporary buffer to hold the current line
+        char line[32];
+        strncpy(line, message + i, line_length);
+        line[line_length] = '\0';
+
+        // If newline was found, skip it for the next iteration
+        if (found_newline)
+        {
+            i += line_length + 1; // +1 to skip the '\n' character
+        }
+        else
+        {
+            // Check if the line ends in the middle of a word and adjust accordingly
+            if (line_length == max_line_length && message[i + line_length] != '\0' && message[i + line_length] != ' ')
+            {
+                // Find the last space within the current line to avoid breaking a word
+                char *last_space = strrchr(line, ' ');
+                if (last_space != NULL)
+                {
+                    // Adjust the line_length to avoid cutting the word
+                    line_length = last_space - line;
+                    line[line_length] = '\0'; // Null-terminate at the space
+                }
+            }
+
+            // Move the index forward by the determined line_length
+            i += line_length;
+
+            // Skip any spaces at the beginning of the next line
+            while (i < message_length && message[i] == ' ')
+            {
+                i++;
+            }
+        }
+
+        // Manually copy the fixed line into the formatted_message buffer
+        for (uint32_t j = 0; j < line_length; j++)
+        {
+            formatted_message[formatted_index++] = line[j];
+        }
+
+        // Add a newline character for line spacing
+        formatted_message[formatted_index++] = '\n';
+    }
+
+    // Null-terminate the formatted_message
+    formatted_message[formatted_index] = '\0';
+
+    // Add the formatted message to the widget
+    widget_add_text_scroll_element(*widget, 0, 0, 128, 64, formatted_message);
+}

+ 39 - 0
callback/loader.h

@@ -0,0 +1,39 @@
+#pragma once
+#include <flip_world.h>
+
+typedef enum DataState DataState;
+enum DataState
+{
+    DataStateInitial,
+    DataStateRequested,
+    DataStateReceived,
+    DataStateParsed,
+    DataStateParseError,
+    DataStateError,
+};
+
+typedef struct DataLoaderModel DataLoaderModel;
+typedef bool (*DataLoaderFetch)(DataLoaderModel *model);
+typedef char *(*DataLoaderParser)(DataLoaderModel *model);
+struct DataLoaderModel
+{
+    char *title;
+    char *data_text;
+    DataState data_state;
+    DataLoaderFetch fetcher;
+    DataLoaderParser parser;
+    void *parser_context;
+    size_t request_index;
+    size_t request_count;
+    ViewNavigationCallback back_callback;
+    FuriTimer *timer;
+    FlipperHTTP *fhttp;
+};
+bool loader_view_alloc(void *context);
+void loader_view_free(void *context);
+void loader_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
+void loader_draw_callback(Canvas *canvas, void *model);
+void loader_init(View *view);
+void loader_widget_set_text(char *message, Widget **widget);
+void loader_free_model(View *view);
+bool loader_custom_event_callback(void *context, uint32_t index);

+ 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;
+};

+ 28 - 9
flip_world.c

@@ -1,17 +1,36 @@
 #include <flip_world.h>
 #include <flip_world.h>
 char *fps_choices_str[] = {"30", "60", "120", "240"};
 char *fps_choices_str[] = {"30", "60", "120", "240"};
-int fps_index = 0;
+uint8_t fps_index = 0;
 char *yes_or_no_choices[] = {"No", "Yes"};
 char *yes_or_no_choices[] = {"No", "Yes"};
-int screen_always_on_index = 1;
-int sound_on_index = 0;
-int vibration_on_index = 0;
+uint8_t screen_always_on_index = 1;
+uint8_t sound_on_index = 1;
+uint8_t vibration_on_index = 1;
 char *player_sprite_choices[] = {"naked", "sword", "axe", "bow"};
 char *player_sprite_choices[] = {"naked", "sword", "axe", "bow"};
-int player_sprite_index = 1;
+uint8_t player_sprite_index = 1;
 char *vgm_levels[] = {"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
 char *vgm_levels[] = {"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
-int vgm_x_index = 2;
-int vgm_y_index = 2;
-int game_mode_index = 0;
+uint8_t vgm_x_index = 2;
+uint8_t vgm_y_index = 2;
+uint8_t game_mode_index = 0;
 float atof_(const char *nptr) { return (float)strtod(nptr, NULL); }
 float atof_(const char *nptr) { return (float)strtod(nptr, NULL); }
 float atof_furi(const FuriString *nptr) { return atof_(furi_string_get_cstr(nptr)); }
 float atof_furi(const FuriString *nptr) { return atof_(furi_string_get_cstr(nptr)); }
 bool is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
 bool is_str(const char *src, const char *dst) { return strcmp(src, dst) == 0; }
-bool is_enough_heap(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } // 1KB buffer
+bool is_enough_heap(size_t heap_size, bool check_blocks)
+{
+    const size_t min_heap = heap_size + 1024; // 1KB buffer
+    const size_t min_free = memmgr_get_free_heap();
+    if (min_free < min_heap)
+    {
+        FURI_LOG_E(TAG, "Not enough heap memory: There are %zu bytes free.", min_free);
+        return false;
+    }
+    if (check_blocks)
+    {
+        const size_t max_free_block = memmgr_heap_get_max_free_block();
+        if (max_free_block < min_heap)
+        {
+            FURI_LOG_E(TAG, "Not enough free blocks: %zu bytes", max_free_block);
+            return false;
+        }
+    }
+    return true;
+}

+ 16 - 11
flip_world.h

@@ -15,7 +15,7 @@
 //
 //
 
 
 #define TAG "FlipWorld"
 #define TAG "FlipWorld"
-#define VERSION 0.7
+#define VERSION 0.8
 #define VERSION_TAG TAG " " FAP_VERSION
 #define VERSION_TAG TAG " " FAP_VERSION
 
 
 // Define the submenu items for our FlipWorld application
 // Define the submenu items for our FlipWorld application
@@ -30,6 +30,7 @@ typedef enum
     FlipWorldSubmenuIndexWiFiSettings,
     FlipWorldSubmenuIndexWiFiSettings,
     FlipWorldSubmenuIndexGameSettings,
     FlipWorldSubmenuIndexGameSettings,
     FlipWorldSubmenuIndexUserSettings,
     FlipWorldSubmenuIndexUserSettings,
+    FlipWorldSubmenuIndexLobby,
 } FlipWorldSubmenuIndex;
 } FlipWorldSubmenuIndex;
 
 
 // Define a single view for our FlipWorld application
 // Define a single view for our FlipWorld application
@@ -37,8 +38,11 @@ typedef enum
 {
 {
     FlipWorldViewSubmenu,          // The submenu
     FlipWorldViewSubmenu,          // The submenu
     FlipWorldViewGameSubmenu,      // The game submenu
     FlipWorldViewGameSubmenu,      // The game submenu
+    FlipWorldViewSubmenuOther,     // The submenu used by settings and lobby
     FlipWorldViewMessage,          // The about, loading screen
     FlipWorldViewMessage,          // The about, loading screen
     FlipWorldViewSettings,         // The settings screen
     FlipWorldViewSettings,         // The settings screen
+    FlipWorldViewLobby,            // The lobby screen
+    FlipWorldViewWaitingLobby,     // The waiting lobby screen
     FlipWorldViewVariableItemList, // The variable item list screen
     FlipWorldViewVariableItemList, // The variable item list screen
     FlipWorldViewTextInput,        // The text input screen
     FlipWorldViewTextInput,        // The text input screen
     //
     //
@@ -63,7 +67,7 @@ typedef struct
     View *view_message;                    // The about, loading screen
     View *view_message;                    // The about, loading screen
     Submenu *submenu;                      // The submenu
     Submenu *submenu;                      // The submenu
     Submenu *submenu_game;                 // The game submenu
     Submenu *submenu_game;                 // The game submenu
-    Submenu *submenu_settings;             // The settings submenu
+    Submenu *submenu_other;                // submenu used by settings and lobby
     VariableItemList *variable_item_list;  // The variable item list (settngs)
     VariableItemList *variable_item_list;  // The variable item list (settngs)
     VariableItem *variable_item_wifi_ssid; // The variable item for WiFi SSID
     VariableItem *variable_item_wifi_ssid; // The variable item for WiFi SSID
     VariableItem *variable_item_wifi_pass; // The variable item for WiFi password
     VariableItem *variable_item_wifi_pass; // The variable item for WiFi password
@@ -84,21 +88,22 @@ typedef struct
     char *text_input_buffer;         // Buffer for the text input
     char *text_input_buffer;         // Buffer for the text input
     char *text_input_temp_buffer;    // Temporary buffer for the text input
     char *text_input_temp_buffer;    // Temporary buffer for the text input
     uint32_t text_input_buffer_size; // Size of the text input buffer
     uint32_t text_input_buffer_size; // Size of the text input buffer
+    //
 } FlipWorldApp;
 } FlipWorldApp;
 
 
 extern char *fps_choices_str[];
 extern char *fps_choices_str[];
-extern int fps_index;
+extern uint8_t fps_index;
 extern char *yes_or_no_choices[];
 extern char *yes_or_no_choices[];
-extern int screen_always_on_index;
-extern int sound_on_index;
-extern int vibration_on_index;
+extern uint8_t screen_always_on_index;
+extern uint8_t sound_on_index;
+extern uint8_t vibration_on_index;
 extern char *player_sprite_choices[];
 extern char *player_sprite_choices[];
-extern int player_sprite_index;
+extern uint8_t player_sprite_index;
 extern char *vgm_levels[];
 extern char *vgm_levels[];
-extern int vgm_x_index;
-extern int vgm_y_index;
-extern int game_mode_index;
+extern uint8_t vgm_x_index;
+extern uint8_t vgm_y_index;
+extern uint8_t game_mode_index;
 float atof_(const char *nptr);
 float atof_(const char *nptr);
 float atof_furi(const FuriString *nptr);
 float atof_furi(const FuriString *nptr);
 bool is_str(const char *src, const char *dst);
 bool is_str(const char *src, const char *dst);
-bool is_enough_heap(size_t heap_size);
+bool is_enough_heap(size_t heap_size, bool check_blocks);

+ 37 - 66
flipper_http/flipper_http.c

@@ -153,22 +153,26 @@ static void get_timeout_timer_callback(void *context)
 }
 }
 
 
 static void flipper_http_rx_callback(const char *line, void *context); // forward declaration
 static void flipper_http_rx_callback(const char *line, void *context); // forward declaration
+// Instead of two globals, we use a single static pointer to the active instance.
+static FlipperHTTP *active_fhttp = NULL;
 
 
-// UART initialization function
-/**
- * @brief      Initialize UART.
- * @return     FlipperHTTP context if the UART was initialized successfully, NULL otherwise.
- * @note       The received data will be handled asynchronously via the callback.
- */
 FlipperHTTP *flipper_http_alloc()
 FlipperHTTP *flipper_http_alloc()
 {
 {
-    FlipperHTTP *fhttp = (FlipperHTTP *)malloc(sizeof(FlipperHTTP));
+    // If an active instance already exists, free it first.
+    if (active_fhttp != NULL)
+    {
+        FURI_LOG_E(HTTP_TAG, "Existing UART instance detected, freeing previous instance.");
+        flipper_http_free(active_fhttp);
+        active_fhttp = NULL;
+    }
+
+    FlipperHTTP *fhttp = malloc(sizeof(FlipperHTTP));
     if (!fhttp)
     if (!fhttp)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP.");
         FURI_LOG_E(HTTP_TAG, "Failed to allocate FlipperHTTP.");
         return NULL;
         return NULL;
     }
     }
-    memset(fhttp, 0, sizeof(FlipperHTTP)); // Initialize allocated memory to zero
+    memset(fhttp, 0, sizeof(FlipperHTTP));
 
 
     fhttp->flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
     fhttp->flipper_http_stream = furi_stream_buffer_alloc(RX_BUF_SIZE, 1);
     if (!fhttp->flipper_http_stream)
     if (!fhttp->flipper_http_stream)
@@ -189,7 +193,7 @@ FlipperHTTP *flipper_http_alloc()
 
 
     furi_thread_set_name(fhttp->rx_thread, "FlipperHTTP_RxThread");
     furi_thread_set_name(fhttp->rx_thread, "FlipperHTTP_RxThread");
     furi_thread_set_stack_size(fhttp->rx_thread, 1024);
     furi_thread_set_stack_size(fhttp->rx_thread, 1024);
-    furi_thread_set_context(fhttp->rx_thread, fhttp); // Corrected context
+    furi_thread_set_context(fhttp->rx_thread, fhttp);
     furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker);
     furi_thread_set_callback(fhttp->rx_thread, flipper_http_worker);
 
 
     fhttp->handle_rx_line_cb = flipper_http_rx_callback;
     fhttp->handle_rx_line_cb = flipper_http_rx_callback;
@@ -198,24 +202,11 @@ FlipperHTTP *flipper_http_alloc()
     furi_thread_start(fhttp->rx_thread);
     furi_thread_start(fhttp->rx_thread);
     fhttp->rx_thread_id = furi_thread_get_id(fhttp->rx_thread);
     fhttp->rx_thread_id = furi_thread_get_id(fhttp->rx_thread);
 
 
-    // Handle when the UART control is busy to avoid furi_check failed
-    if (furi_hal_serial_control_is_busy(UART_CH))
-    {
-        FURI_LOG_E(HTTP_TAG, "UART control is busy.");
-        // Cleanup resources
-        furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
-        furi_thread_join(fhttp->rx_thread);
-        furi_thread_free(fhttp->rx_thread);
-        furi_stream_buffer_free(fhttp->flipper_http_stream);
-        free(fhttp);
-        return NULL;
-    }
-
+    // Acquire UART control
     fhttp->serial_handle = furi_hal_serial_control_acquire(UART_CH);
     fhttp->serial_handle = furi_hal_serial_control_acquire(UART_CH);
     if (!fhttp->serial_handle)
     if (!fhttp->serial_handle)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
         FURI_LOG_E(HTTP_TAG, "Failed to acquire UART control - handle is NULL");
-        // Cleanup resources
         furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
         furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
         furi_thread_join(fhttp->rx_thread);
         furi_thread_join(fhttp->rx_thread);
         furi_thread_free(fhttp->rx_thread);
         furi_thread_free(fhttp->rx_thread);
@@ -224,29 +215,17 @@ FlipperHTTP *flipper_http_alloc()
         return NULL;
         return NULL;
     }
     }
 
 
-    // Initialize UART with acquired handle
+    // Initialize and enable UART
     furi_hal_serial_init(fhttp->serial_handle, BAUDRATE);
     furi_hal_serial_init(fhttp->serial_handle, BAUDRATE);
-
-    // Enable RX direction
     furi_hal_serial_enable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
     furi_hal_serial_enable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
-
-    // Start asynchronous RX with the corrected callback and context
-    furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false); // Corrected context
-
-    // Wait for the TX to complete to ensure UART is ready
+    furi_hal_serial_async_rx_start(fhttp->serial_handle, _flipper_http_rx_callback, fhttp, false);
     furi_hal_serial_tx_wait_complete(fhttp->serial_handle);
     furi_hal_serial_tx_wait_complete(fhttp->serial_handle);
 
 
-    // Allocate the timer for handling timeouts
-    fhttp->get_timeout_timer = furi_timer_alloc(
-        get_timeout_timer_callback, // Callback function
-        FuriTimerTypeOnce,          // One-shot timer
-        fhttp                       // Corrected context
-    );
-
+    // Allocate the timeout timer
+    fhttp->get_timeout_timer = furi_timer_alloc(get_timeout_timer_callback, FuriTimerTypeOnce, fhttp);
     if (!fhttp->get_timeout_timer)
     if (!fhttp->get_timeout_timer)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer.");
         FURI_LOG_E(HTTP_TAG, "Failed to allocate HTTP request timeout timer.");
-        // Cleanup resources
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
         furi_hal_serial_control_release(fhttp->serial_handle);
         furi_hal_serial_control_release(fhttp->serial_handle);
@@ -258,15 +237,12 @@ FlipperHTTP *flipper_http_alloc()
         free(fhttp);
         free(fhttp);
         return NULL;
         return NULL;
     }
     }
-
-    // Set the timer thread priority if needed
     furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
     furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
 
 
-    fhttp->last_response = (char *)malloc(RX_BUF_SIZE);
+    fhttp->last_response = malloc(RX_BUF_SIZE);
     if (!fhttp->last_response)
     if (!fhttp->last_response)
     {
     {
         FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response.");
         FURI_LOG_E(HTTP_TAG, "Failed to allocate memory for last_response.");
-        // Cleanup resources
         furi_timer_free(fhttp->get_timeout_timer);
         furi_timer_free(fhttp->get_timeout_timer);
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_async_rx_stop(fhttp->serial_handle);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
         furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
@@ -279,21 +255,15 @@ FlipperHTTP *flipper_http_alloc()
         free(fhttp);
         free(fhttp);
         return NULL;
         return NULL;
     }
     }
-    memset(fhttp->last_response, 0, RX_BUF_SIZE); // Initialize last_response
-
+    memset(fhttp->last_response, 0, RX_BUF_SIZE);
     fhttp->state = IDLE;
     fhttp->state = IDLE;
 
 
-    // FURI_LOG_I(HTTP_TAG, "UART initialized successfully.");
+    // Track the active instance globally.
+    active_fhttp = fhttp;
+
     return fhttp;
     return fhttp;
 }
 }
 
 
-// Deinitialize UART
-/**
- * @brief      Deinitialize UART.
- * @return     void
- * @param fhttp The FlipperHTTP context
- * @note       This function will stop the asynchronous RX, release the serial handle, and free the resources.
- */
 void flipper_http_free(FlipperHTTP *fhttp)
 void flipper_http_free(FlipperHTTP *fhttp)
 {
 {
     if (!fhttp)
     if (!fhttp)
@@ -306,43 +276,42 @@ void flipper_http_free(FlipperHTTP *fhttp)
         FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
         FURI_LOG_E(HTTP_TAG, "UART handle is NULL. Already deinitialized?");
         return;
         return;
     }
     }
-    // Stop asynchronous RX
+    // Stop asynchronous RX and clean up UART
     furi_hal_serial_async_rx_stop(fhttp->serial_handle);
     furi_hal_serial_async_rx_stop(fhttp->serial_handle);
-
-    // Release and deinitialize the serial handle
     furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
     furi_hal_serial_disable_direction(fhttp->serial_handle, FuriHalSerialDirectionRx);
-    furi_hal_serial_control_release(fhttp->serial_handle);
     furi_hal_serial_deinit(fhttp->serial_handle);
     furi_hal_serial_deinit(fhttp->serial_handle);
+    furi_hal_serial_control_release(fhttp->serial_handle);
 
 
-    // Signal the worker thread to stop
+    // Signal and free the worker thread
     furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
     furi_thread_flags_set(fhttp->rx_thread_id, WorkerEvtStop);
-    // Wait for the thread to finish
     furi_thread_join(fhttp->rx_thread);
     furi_thread_join(fhttp->rx_thread);
-    // Free the thread resources
     furi_thread_free(fhttp->rx_thread);
     furi_thread_free(fhttp->rx_thread);
 
 
     // Free the stream buffer
     // Free the stream buffer
     furi_stream_buffer_free(fhttp->flipper_http_stream);
     furi_stream_buffer_free(fhttp->flipper_http_stream);
 
 
-    // Free the timer
+    // Free the timer, if allocated
     if (fhttp->get_timeout_timer)
     if (fhttp->get_timeout_timer)
     {
     {
         furi_timer_free(fhttp->get_timeout_timer);
         furi_timer_free(fhttp->get_timeout_timer);
         fhttp->get_timeout_timer = NULL;
         fhttp->get_timeout_timer = NULL;
     }
     }
 
 
-    // Free the last response
+    // Free the last_response buffer
     if (fhttp->last_response)
     if (fhttp->last_response)
     {
     {
         free(fhttp->last_response);
         free(fhttp->last_response);
         fhttp->last_response = NULL;
         fhttp->last_response = NULL;
     }
     }
 
 
-    // Free the FlipperHTTP context
-    free(fhttp);
-    fhttp = NULL;
+    // If this instance is the active instance, clear the static pointer.
+    if (active_fhttp == fhttp)
+    {
+        free(active_fhttp);
+        active_fhttp = NULL;
+    }
 
 
-    // FURI_LOG_I("FlipperHTTP", "UART deinitialized successfully.");
+    free(fhttp);
 }
 }
 
 
 /**
 /**
@@ -945,6 +914,8 @@ bool flipper_http_send_command(FlipperHTTP *fhttp, HTTPCommand command)
     case HTTP_CMD_PING:
     case HTTP_CMD_PING:
         fhttp->state = INACTIVE; // set state as INACTIVE to be made IDLE if PONG is received
         fhttp->state = INACTIVE; // set state as INACTIVE to be made IDLE if PONG is received
         return flipper_http_send_data(fhttp, "[PING]");
         return flipper_http_send_data(fhttp, "[PING]");
+    case HTTP_CMD_REBOOT:
+        return flipper_http_send_data(fhttp, "[REBOOT]");
     default:
     default:
         FURI_LOG_E(HTTP_TAG, "Invalid command.");
         FURI_LOG_E(HTTP_TAG, "Invalid command.");
         return false;
         return false;

+ 3 - 3
flipper_http/flipper_http.h

@@ -20,7 +20,6 @@
 
 
 #undef FURI_LOG_I
 #undef FURI_LOG_I
 #define FURI_LOG_I(tag, msg, ...)
 #define FURI_LOG_I(tag, msg, ...)
-//
 
 
 #define HTTP_TAG "FlipWorld"              // change this to your app name
 #define HTTP_TAG "FlipWorld"              // change this to your app name
 #define http_tag "flip_world"             // change this to your app id
 #define http_tag "flip_world"             // change this to your app id
@@ -28,7 +27,7 @@
 #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds
 #define TIMEOUT_DURATION_TICKS (5 * 1000) // 5 seconds
 #define BAUDRATE (115200)                 // UART baudrate
 #define BAUDRATE (115200)                 // UART baudrate
 #define RX_BUF_SIZE 2048                  // UART RX buffer size
 #define RX_BUF_SIZE 2048                  // UART RX buffer size
-#define RX_LINE_BUFFER_SIZE 3000          // UART RX line buffer size (increase for large responses)
+#define RX_LINE_BUFFER_SIZE 1024          // UART RX line buffer size (increase for large responses)
 #define MAX_FILE_SHOW 3000                // Maximum data from file to show
 #define MAX_FILE_SHOW 3000                // Maximum data from file to show
 #define FILE_BUFFER_SIZE 512              // File buffer size
 #define FILE_BUFFER_SIZE 512              // File buffer size
 
 
@@ -73,7 +72,8 @@ typedef enum
     HTTP_CMD_LIST_COMMANDS,
     HTTP_CMD_LIST_COMMANDS,
     HTTP_CMD_LED_ON,
     HTTP_CMD_LED_ON,
     HTTP_CMD_LED_OFF,
     HTTP_CMD_LED_OFF,
-    HTTP_CMD_PING
+    HTTP_CMD_PING,
+    HTTP_CMD_REBOOT
 } HTTPCommand; // list of non-input commands
 } HTTPCommand; // list of non-input commands
 
 
 // FlipperHTTP Structure
 // FlipperHTTP Structure

+ 9 - 7
game/draw.c

@@ -8,7 +8,7 @@ int camera_y = 0;
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
 void draw_user_stats(Canvas *canvas, Vector pos, GameManager *manager)
 {
 {
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
-    PlayerContext *player = game_context->player_context;
+    PlayerContext *player = entity_context_get(game_context->player);
 
 
     // first draw a black rectangle to make the text more readable
     // first draw a black rectangle to make the text more readable
     canvas_invert_color(canvas);
     canvas_invert_color(canvas);
@@ -87,6 +87,7 @@ void spawn_icon_line(GameManager *manager, Level *level, const char *icon_id, fl
 static void draw_menu(GameManager *manager, Canvas *canvas)
 static void draw_menu(GameManager *manager, Canvas *canvas)
 {
 {
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
+    PlayerContext *player_context = entity_context_get(game_context->player);
 
 
     // draw background rectangle
     // draw background rectangle
     canvas_draw_icon(
     canvas_draw_icon(
@@ -115,12 +116,12 @@ static void draw_menu(GameManager *manager, Canvas *canvas)
             char level[32];
             char level[32];
             char strength[32];
             char strength[32];
 
 
-            snprintf(level, sizeof(level), "Level   : %ld", game_context->player_context->level);
-            snprintf(health, sizeof(health), "Health  : %ld", game_context->player_context->health);
-            snprintf(xp, sizeof(xp), "XP      : %ld", game_context->player_context->xp);
-            snprintf(strength, sizeof(strength), "Strength: %ld", game_context->player_context->strength);
+            snprintf(level, sizeof(level), "Level   : %ld", player_context->level);
+            snprintf(health, sizeof(health), "Health  : %ld", player_context->health);
+            snprintf(xp, sizeof(xp), "XP      : %ld", player_context->xp);
+            snprintf(strength, sizeof(strength), "Strength: %ld", player_context->strength);
             canvas_set_font(canvas, FontPrimary);
             canvas_set_font(canvas, FontPrimary);
-            canvas_draw_str(canvas, 7, 16, game_context->player_context->username);
+            canvas_draw_str(canvas, 7, 16, player_context->username);
             canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
             canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
             canvas_draw_str(canvas, 7, 30, level);
             canvas_draw_str(canvas, 7, 30, level);
             canvas_draw_str(canvas, 7, 37, health);
             canvas_draw_str(canvas, 7, 37, health);
@@ -177,13 +178,14 @@ void background_render(Canvas *canvas, GameManager *manager)
         return;
         return;
 
 
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
+    PlayerContext *player_context = entity_context_get(game_context->player);
     if (!game_context->is_menu_open)
     if (!game_context->is_menu_open)
     {
     {
         // get player position
         // get player position
         Vector posi = entity_pos_get(game_context->player);
         Vector posi = entity_pos_get(game_context->player);
 
 
         // draw username over player's head
         // draw username over player's head
-        draw_username(canvas, posi, game_context->player_context->username);
+        draw_username(canvas, posi, player_context->username);
 
 
         if (game_context->is_switching_level)
         if (game_context->is_switching_level)
             // draw switch world icon
             // draw switch world icon

+ 307 - 131
game/enemy.c

@@ -1,9 +1,10 @@
 // enemy.c
 // enemy.c
 #include <game/enemy.h>
 #include <game/enemy.h>
 #include <notification/notification_messages.h>
 #include <notification/notification_messages.h>
+#include <flip_storage/storage.h>
+#include <game/storage.h>
 
 
 static EntityContext *enemy_context_generic;
 static EntityContext *enemy_context_generic;
-
 // Allocation function
 // Allocation function
 static EntityContext *enemy_generic_alloc(
 static EntityContext *enemy_generic_alloc(
     const char *id,
     const char *id,
@@ -15,7 +16,9 @@ static EntityContext *enemy_generic_alloc(
     float speed,
     float speed,
     float attack_timer,
     float attack_timer,
     float strength,
     float strength,
-    float health)
+    float health,
+    bool is_user,
+    FuriString *username)
 {
 {
     if (!enemy_context_generic)
     if (!enemy_context_generic)
     {
     {
@@ -44,6 +47,17 @@ static EntityContext *enemy_generic_alloc(
     enemy_context_generic->state = ENTITY_MOVING_TO_END; // Start in IDLE state
     enemy_context_generic->state = ENTITY_MOVING_TO_END; // Start in IDLE state
     // Set radius based on size, for example, average of size.x and size.y divided by 2
     // Set radius based on size, for example, average of size.x and size.y divided by 2
     enemy_context_generic->radius = (size.x + size.y) / 4.0f;
     enemy_context_generic->radius = (size.x + size.y) / 4.0f;
+    //
+    enemy_context_generic->is_user = is_user;
+    //
+    if (username != NULL)
+    {
+        snprintf(enemy_context_generic->username, sizeof(enemy_context_generic->username), "%s", furi_string_get_cstr(username));
+    }
+    else
+    {
+        snprintf(enemy_context_generic->username, sizeof(enemy_context_generic->username), "SYSTEM_ENEMY");
+    }
     return enemy_context_generic;
     return enemy_context_generic;
 }
 }
 
 
@@ -80,6 +94,8 @@ static void enemy_start(Entity *self, GameManager *manager, void *context)
     enemy_context->direction = enemy_context_generic->direction;
     enemy_context->direction = enemy_context_generic->direction;
     enemy_context->state = enemy_context_generic->state;
     enemy_context->state = enemy_context_generic->state;
     enemy_context->radius = enemy_context_generic->radius;
     enemy_context->radius = enemy_context_generic->radius;
+    enemy_context->is_user = enemy_context_generic->is_user;
+    snprintf(enemy_context->username, sizeof(enemy_context->username), "%s", enemy_context_generic->username);
 
 
     // Set enemy's initial position based on start_position
     // Set enemy's initial position based on start_position
     entity_pos_set(self, enemy_context->start_position);
     entity_pos_set(self, enemy_context->start_position);
@@ -210,6 +226,7 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
     furi_check(enemy_context, "Enemy collision: EntityContext is NULL");
     furi_check(enemy_context, "Enemy collision: EntityContext is NULL");
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
     furi_check(game_context, "Enemy collision: GameContext is NULL");
     furi_check(game_context, "Enemy collision: GameContext is NULL");
+    PlayerContext *player_context = entity_context_get(game_context->player);
     if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step != 4)
     if (game_context->game_mode == GAME_MODE_STORY && game_context->tutorial_step != 4)
     {
     {
         // FURI_LOG_I("Game", "Enemy collision: No enemies in story mode");
         // FURI_LOG_I("Game", "Enemy collision: No enemies in story mode");
@@ -237,10 +254,10 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
         }
         }
 
 
         // Determine if the player is facing the enemy
         // Determine if the player is facing the enemy
-        if ((game_context->player_context->direction == ENTITY_LEFT && enemy_pos.x < player_pos.x) ||
-            (game_context->player_context->direction == ENTITY_RIGHT && enemy_pos.x > player_pos.x) ||
-            (game_context->player_context->direction == ENTITY_UP && enemy_pos.y < player_pos.y) ||
-            (game_context->player_context->direction == ENTITY_DOWN && enemy_pos.y > player_pos.y))
+        if ((player_context->direction == ENTITY_LEFT && enemy_pos.x < player_pos.x) ||
+            (player_context->direction == ENTITY_RIGHT && enemy_pos.x > player_pos.x) ||
+            (player_context->direction == ENTITY_UP && enemy_pos.y < player_pos.y) ||
+            (player_context->direction == ENTITY_DOWN && enemy_pos.y > player_pos.y))
         {
         {
             player_is_facing_enemy = true;
             player_is_facing_enemy = true;
         }
         }
@@ -256,31 +273,41 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
             // Reset last button
             // Reset last button
             game_context->last_button = -1;
             game_context->last_button = -1;
 
 
-            if (game_context->player_context->elapsed_attack_timer >= game_context->player_context->attack_timer)
+            if (player_context->elapsed_attack_timer >= player_context->attack_timer)
             {
             {
                 atk_notify(game_context, enemy_context, true);
                 atk_notify(game_context, enemy_context, true);
 
 
                 // Reset player's elapsed attack timer
                 // Reset player's elapsed attack timer
-                game_context->player_context->elapsed_attack_timer = 0.0f;
+                player_context->elapsed_attack_timer = 0.0f;
                 enemy_context->elapsed_attack_timer = 0.0f; // Reset enemy's attack timer to block enemy attack
                 enemy_context->elapsed_attack_timer = 0.0f; // Reset enemy's attack timer to block enemy attack
 
 
                 // Increase XP by the enemy's strength
                 // Increase XP by the enemy's strength
-                game_context->player_context->xp += enemy_context->strength;
+                player_context->xp += enemy_context->strength;
 
 
                 // Increase healthy by 10% of the enemy's strength
                 // Increase healthy by 10% of the enemy's strength
-                game_context->player_context->health += enemy_context->strength * 0.1f;
-                if (game_context->player_context->health > game_context->player_context->max_health)
+                player_context->health += enemy_context->strength * 0.1f;
+                if (player_context->health > player_context->max_health)
                 {
                 {
-                    game_context->player_context->health = game_context->player_context->max_health;
+                    player_context->health = player_context->max_health;
                 }
                 }
 
 
                 // Decrease enemy health by player strength
                 // Decrease enemy health by player strength
-                enemy_context->health -= game_context->player_context->strength;
+                enemy_context->health -= player_context->strength;
 
 
                 if (enemy_context->health <= 0)
                 if (enemy_context->health <= 0)
                 {
                 {
                     enemy_context->state = ENTITY_DEAD;
                     enemy_context->state = ENTITY_DEAD;
 
 
+                    // if pvp, end the game
+                    if (game_context->game_mode == GAME_MODE_PVP)
+                    {
+                        player_context->health = player_context->max_health;
+                        save_player_context(player_context);
+                        furi_delay_ms(100);
+                        game_manager_game_stop(manager);
+                        return;
+                    }
+
                     // Reset enemy position and health
                     // Reset enemy position and health
                     enemy_context->health = 100; // this needs to be set to the enemy's max health
                     enemy_context->health = 100; // this needs to be set to the enemy's max health
 
 
@@ -296,18 +323,18 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                     enemy_context->state = ENTITY_ATTACKED;
                     enemy_context->state = ENTITY_ATTACKED;
                     // Vector old_pos = entity_pos_get(self);
                     // Vector old_pos = entity_pos_get(self);
                     //  Bounce the enemy back by X units opposite their last movement direction
                     //  Bounce the enemy back by X units opposite their last movement direction
-                    enemy_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
-                    // enemy_pos.y -= game_context->player_context->dy * enemy_context->radius + game_context->icon_offset;
+                    enemy_pos.x -= player_context->dx * enemy_context->radius + game_context->icon_offset;
+                    // enemy_pos.y -= player_context->dy * enemy_context->radius + game_context->icon_offset;
                     entity_pos_set(self, enemy_pos);
                     entity_pos_set(self, enemy_pos);
 
 
                     // Reset enemy's movement direction to prevent immediate re-collision
                     // Reset enemy's movement direction to prevent immediate re-collision
-                    game_context->player_context->dx = 0;
-                    game_context->player_context->dy = 0;
+                    player_context->dx = 0;
+                    player_context->dy = 0;
                 }
                 }
             }
             }
             else
             else
             {
             {
-                FURI_LOG_I("Game", "Player attack on enemy '%s' is on cooldown: %f seconds remaining", enemy_context->id, (double)(game_context->player_context->attack_timer - game_context->player_context->elapsed_attack_timer));
+                FURI_LOG_I("Game", "Player attack on enemy '%s' is on cooldown: %f seconds remaining", enemy_context->id, (double)(player_context->attack_timer - player_context->elapsed_attack_timer));
             }
             }
         }
         }
         // Handle Enemy Attacking Player (enemy facing player)
         // Handle Enemy Attacking Player (enemy facing player)
@@ -321,54 +348,63 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
                 enemy_context->elapsed_attack_timer = 0.0f;
                 enemy_context->elapsed_attack_timer = 0.0f;
 
 
                 // Decrease player health by enemy strength
                 // Decrease player health by enemy strength
-                game_context->player_context->health -= enemy_context->strength;
+                player_context->health -= enemy_context->strength;
 
 
-                if (game_context->player_context->health <= 0)
+                if (player_context->health <= 0)
                 {
                 {
                     FURI_LOG_I("Game", "Player is dead.. resetting player position and health");
                     FURI_LOG_I("Game", "Player is dead.. resetting player position and health");
-                    game_context->player_context->state = ENTITY_DEAD;
+                    player_context->state = ENTITY_DEAD;
+
+                    // if pvp, end the game
+                    if (game_context->game_mode == GAME_MODE_PVP)
+                    {
+                        save_player_context(player_context);
+                        furi_delay_ms(100);
+                        game_manager_game_stop(manager);
+                        return;
+                    }
 
 
                     // Reset player position and health
                     // Reset player position and health
-                    entity_pos_set(other, game_context->player_context->start_position);
-                    game_context->player_context->health = game_context->player_context->max_health;
+                    entity_pos_set(other, player_context->start_position);
+                    player_context->health = player_context->max_health;
 
 
                     // subtract player's XP by the enemy's strength
                     // subtract player's XP by the enemy's strength
-                    game_context->player_context->xp -= enemy_context->strength;
-                    if ((int)game_context->player_context->xp < 0)
+                    player_context->xp -= enemy_context->strength;
+                    if ((int)player_context->xp < 0)
                     {
                     {
-                        game_context->player_context->xp = 0;
+                        player_context->xp = 0;
                     }
                     }
                 }
                 }
                 else
                 else
                 {
                 {
                     FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
                     FURI_LOG_I("Game", "Player took %f damage from enemy '%s'", (double)enemy_context->strength, enemy_context->id);
-                    game_context->player_context->state = ENTITY_ATTACKED;
+                    player_context->state = ENTITY_ATTACKED;
 
 
                     // Bounce the player back by X units opposite their last movement direction
                     // Bounce the player back by X units opposite their last movement direction
-                    player_pos.x -= game_context->player_context->dx * enemy_context->radius + game_context->icon_offset;
-                    // player_pos.y -= game_context->player_context->dy * enemy_context->radius + game_context->icon_offset;
+                    player_pos.x -= player_context->dx * enemy_context->radius + game_context->icon_offset;
+                    // player_pos.y -= player_context->dy * enemy_context->radius + game_context->icon_offset;
                     entity_pos_set(other, player_pos);
                     entity_pos_set(other, player_pos);
 
 
                     // Reset player's movement direction to prevent immediate re-collision
                     // Reset player's movement direction to prevent immediate re-collision
-                    game_context->player_context->dx = 0;
-                    game_context->player_context->dy = 0;
+                    player_context->dx = 0;
+                    player_context->dy = 0;
                 }
                 }
             }
             }
         }
         }
         else // handle other collisions
         else // handle other collisions
         {
         {
             // Set the player's old position to prevent collision
             // Set the player's old position to prevent collision
-            entity_pos_set(other, game_context->player_context->old_position);
+            entity_pos_set(other, player_context->old_position);
             // Reset player's movement direction to prevent immediate re-collision
             // Reset player's movement direction to prevent immediate re-collision
-            game_context->player_context->dx = 0;
-            game_context->player_context->dy = 0;
+            player_context->dx = 0;
+            player_context->dy = 0;
         }
         }
 
 
-        if (game_context->player_context->state == ENTITY_DEAD)
+        if (player_context->state == ENTITY_DEAD)
         {
         {
             // Reset player's position and health
             // Reset player's position and health
-            entity_pos_set(other, game_context->player_context->start_position);
-            game_context->player_context->health = game_context->player_context->max_health;
+            entity_pos_set(other, player_context->start_position);
+            player_context->health = player_context->max_health;
         }
         }
     }
     }
     // if not player than must be an icon or npc; so push back
     // if not player than must be an icon or npc; so push back
@@ -397,6 +433,113 @@ static void enemy_collision(Entity *self, Entity *other, GameManager *manager, v
     }
     }
 }
 }
 
 
+static void pvp_position(GameContext *game_context, EntityContext *enemy, Entity *self)
+{
+    if (!game_context || !enemy || !self)
+    {
+        FURI_LOG_E("Game", "PVP position: Invalid parameters");
+        return;
+    }
+
+    if (game_context->fhttp->last_response != NULL && strlen(game_context->fhttp->last_response) > 0)
+    {
+        // for debugging
+        // save_char("received_pvp_position", game_context->fhttp->last_response);
+        // parse the response and set the enemy position
+        /* expected response:
+        {
+            "u": "JBlanked",
+            "xp": 37743,
+            "h": 207,
+            "ehr": 0.7,
+            "eat": 127.5,
+            "d": 2,
+            "s": 1,
+            "sp": {
+                "x": 381.0,
+                "y": 192.0
+            }
+        }
+        */
+
+        // FuriStrings are probably safer but we already last_response as a char*
+
+        // match username
+        char *u = get_json_value("u", game_context->fhttp->last_response);
+        if (!u || !is_str(u, enemy->username))
+        {
+            if (u)
+                free(u);
+            return;
+        }
+
+        // we need the health, elapsed attack timer, direction, and position
+        char *h = get_json_value("h", game_context->fhttp->last_response);
+        char *eat = get_json_value("eat", game_context->fhttp->last_response);
+        char *d = get_json_value("d", game_context->fhttp->last_response);
+        char *sp = get_json_value("sp", game_context->fhttp->last_response);
+        char *x = get_json_value("x", sp);
+        char *y = get_json_value("y", sp);
+
+        if (!h || !eat || !d || !sp || !x || !y)
+        {
+            if (h)
+                free(h);
+            if (eat)
+                free(eat);
+            if (d)
+                free(d);
+            if (sp)
+                free(sp);
+            if (x)
+                free(x);
+            if (y)
+                free(y);
+            free(u);
+            return;
+        }
+
+        // set enemy info
+        enemy->health = (float)atoi(h);
+        enemy->elapsed_attack_timer = (float)atof_(eat);
+        switch (atoi(d))
+        {
+        case 0:
+            enemy->direction = ENTITY_LEFT;
+            break;
+        case 1:
+            enemy->direction = ENTITY_RIGHT;
+            break;
+        case 2:
+            enemy->direction = ENTITY_UP;
+            break;
+        case 3:
+            enemy->direction = ENTITY_DOWN;
+            break;
+        default:
+            enemy->direction = ENTITY_RIGHT;
+            break;
+        }
+
+        Vector new_pos = (Vector){
+            .x = (float)atof_(x),
+            .y = (float)atof_(y),
+        };
+
+        // set enemy position
+        entity_pos_set(self, new_pos);
+
+        // free the strings
+        free(h);
+        free(eat);
+        free(d);
+        free(sp);
+        free(x);
+        free(y);
+        free(u);
+    }
+}
+
 // Enemy update function
 // Enemy update function
 static void enemy_update(Entity *self, GameManager *manager, void *context)
 static void enemy_update(Entity *self, GameManager *manager, void *context)
 {
 {
@@ -416,117 +559,127 @@ static void enemy_update(Entity *self, GameManager *manager, void *context)
         return;
         return;
     }
     }
 
 
-    float delta_time = 1.0f / game_context->fps;
+    const float delta_time = 1.0f / game_context->fps;
 
 
-    // Increment the elapsed_attack_timer for the enemy
-    enemy_context->elapsed_attack_timer += delta_time;
-
-    switch (enemy_context->state)
+    if (game_context->game_mode == GAME_MODE_PVP)
     {
     {
-    case ENTITY_IDLE:
-        // Increment the elapsed_move_timer
-        enemy_context->elapsed_move_timer += delta_time;
+        // update enemy position
+        pvp_position(game_context, enemy_context, self);
+    }
+    else
+    {
+        // Increment the elapsed_attack_timer for the enemy
+        enemy_context->elapsed_attack_timer += delta_time;
 
 
-        // Check if it's time to move again
-        if (enemy_context->elapsed_move_timer >= enemy_context->move_timer)
+        switch (enemy_context->state)
         {
         {
-            // Determine the next state based on the current position
-            Vector current_pos = entity_pos_get(self);
-            if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 &&
-                fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0)
-            {
-                enemy_context->state = ENTITY_MOVING_TO_END;
-            }
-            else
+        case ENTITY_IDLE:
+            // Increment the elapsed_move_timer
+            enemy_context->elapsed_move_timer += delta_time;
+
+            // Check if it's time to move again
+            if (enemy_context->elapsed_move_timer >= enemy_context->move_timer)
             {
             {
-                enemy_context->state = ENTITY_MOVING_TO_START;
+                // Determine the next state based on the current position
+                Vector current_pos = entity_pos_get(self);
+                if (fabs(current_pos.x - enemy_context->start_position.x) < (double)1.0 &&
+                    fabs(current_pos.y - enemy_context->start_position.y) < (double)1.0)
+                {
+                    enemy_context->state = ENTITY_MOVING_TO_END;
+                }
+                else
+                {
+                    enemy_context->state = ENTITY_MOVING_TO_START;
+                }
+                enemy_context->elapsed_move_timer = 0.0f;
             }
             }
-            enemy_context->elapsed_move_timer = 0.0f;
-        }
-        break;
 
 
-    case ENTITY_MOVING_TO_END:
-    case ENTITY_MOVING_TO_START:
-    case ENTITY_ATTACKED:
-    {
-        // Get current position
-        Vector current_pos = entity_pos_get(self);
-        if (enemy_context->state == ENTITY_ATTACKED)
+            break;
+
+        case ENTITY_MOVING_TO_END:
+        case ENTITY_MOVING_TO_START:
+        case ENTITY_ATTACKED:
         {
         {
-            // set direction again
-            enemy_context->state = enemy_context->direction == ENTITY_LEFT ? ENTITY_MOVING_TO_START : ENTITY_MOVING_TO_END;
-        }
+            // Get current position
+            Vector current_pos = entity_pos_get(self);
+            if (enemy_context->state == ENTITY_ATTACKED)
+            {
+                // set direction again
+                enemy_context->state = enemy_context->direction == ENTITY_LEFT ? ENTITY_MOVING_TO_START : ENTITY_MOVING_TO_END;
+            }
 
 
-        // Determine the target position based on the current state
-        Vector target_position = (enemy_context->state == ENTITY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
-        Vector direction_vector = {0, 0};
+            // Determine the target position based on the current state
+            Vector target_position = (enemy_context->state == ENTITY_MOVING_TO_END) ? enemy_context->end_position : enemy_context->start_position;
+            Vector direction_vector = {0, 0};
 
 
-        // Calculate direction towards the target
-        if (current_pos.x < target_position.x)
-        {
-            direction_vector.x = 1.0f;
-            enemy_context->direction = ENTITY_RIGHT;
-        }
-        else if (current_pos.x > target_position.x)
-        {
-            direction_vector.x = -1.0f;
-            enemy_context->direction = ENTITY_LEFT;
-        }
+            // Calculate direction towards the target
+            if (current_pos.x < target_position.x)
+            {
+                direction_vector.x = 1.0f;
+                enemy_context->direction = ENTITY_RIGHT;
+            }
+            else if (current_pos.x > target_position.x)
+            {
+                direction_vector.x = -1.0f;
+                enemy_context->direction = ENTITY_LEFT;
+            }
 
 
-        if (current_pos.y < target_position.y)
-        {
-            direction_vector.y = 1.0f;
-            enemy_context->direction = ENTITY_DOWN;
-        }
-        else if (current_pos.y > target_position.y)
-        {
-            direction_vector.y = -1.0f;
-            enemy_context->direction = ENTITY_UP;
-        }
+            if (current_pos.y < target_position.y)
+            {
+                direction_vector.y = 1.0f;
+                enemy_context->direction = ENTITY_DOWN;
+            }
+            else if (current_pos.y > target_position.y)
+            {
+                direction_vector.y = -1.0f;
+                enemy_context->direction = ENTITY_UP;
+            }
 
 
-        // Normalize direction vector
-        float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
-        if (length != 0)
-        {
-            direction_vector.x /= length;
-            direction_vector.y /= length;
-        }
+            // Normalize direction vector
+            float length = sqrt(direction_vector.x * direction_vector.x + direction_vector.y * direction_vector.y);
+            if (length != 0)
+            {
+                direction_vector.x /= length;
+                direction_vector.y /= length;
+            }
 
 
-        // Update position based on direction and speed
-        Vector new_pos = current_pos;
-        new_pos.x += direction_vector.x * enemy_context->speed * delta_time;
-        new_pos.y += direction_vector.y * enemy_context->speed * delta_time;
+            // Update position based on direction and speed
+            Vector new_pos = current_pos;
+            new_pos.x += direction_vector.x * enemy_context->speed * delta_time;
+            new_pos.y += direction_vector.y * enemy_context->speed * delta_time;
 
 
-        // Clamp the position to the target to prevent overshooting
-        if ((direction_vector.x > 0.0f && new_pos.x > target_position.x) ||
-            (direction_vector.x < 0.0f && new_pos.x < target_position.x))
-        {
-            new_pos.x = target_position.x;
-        }
+            // Clamp the position to the target to prevent overshooting
+            if ((direction_vector.x > 0.0f && new_pos.x > target_position.x) ||
+                (direction_vector.x < 0.0f && new_pos.x < target_position.x))
+            {
+                new_pos.x = target_position.x;
+            }
 
 
-        if ((direction_vector.y > 0.0f && new_pos.y > target_position.y) ||
-            (direction_vector.y < 0.0f && new_pos.y < target_position.y))
-        {
-            new_pos.y = target_position.y;
-        }
+            if ((direction_vector.y > 0.0f && new_pos.y > target_position.y) ||
+                (direction_vector.y < 0.0f && new_pos.y < target_position.y))
+            {
+                new_pos.y = target_position.y;
+            }
 
 
-        entity_pos_set(self, new_pos);
+            // Set the new position
+            entity_pos_set(self, new_pos);
 
 
-        // Check if the enemy has reached or surpassed the target_position
-        bool reached_x = fabs(new_pos.x - target_position.x) < (double)1.0;
-        bool reached_y = fabs(new_pos.y - target_position.y) < (double)1.0;
+            // Check if the enemy has reached or surpassed the target_position
+            bool reached_x = fabs(new_pos.x - target_position.x) < (double)1.0;
+            bool reached_y = fabs(new_pos.y - target_position.y) < (double)1.0;
 
 
-        // If reached the target position on both axes, transition to IDLE
-        if (reached_x && reached_y)
-        {
-            enemy_context->state = ENTITY_IDLE;
-            enemy_context->elapsed_move_timer = 0.0f;
+            // If reached the target position on both axes, transition to IDLE
+            if (reached_x && reached_y)
+            {
+                enemy_context->state = ENTITY_IDLE;
+                enemy_context->elapsed_move_timer = 0.0f;
+            }
         }
         }
-    }
-    break;
-
-    default:
         break;
         break;
+
+        default:
+            break;
+        }
     }
     }
 }
 }
 
 
@@ -565,7 +718,10 @@ const EntityDescription *enemy(
     float speed,
     float speed,
     float attack_timer,
     float attack_timer,
     float strength,
     float strength,
-    float health)
+    float health,
+    bool is_user,
+    FuriString *username)
+
 {
 {
     SpriteContext *sprite_context = get_sprite_context(id);
     SpriteContext *sprite_context = get_sprite_context(id);
     if (!sprite_context)
     if (!sprite_context)
@@ -585,7 +741,8 @@ const EntityDescription *enemy(
         speed,
         speed,
         attack_timer,
         attack_timer,
         strength,
         strength,
-        health);
+        health,
+        is_user, username);
     if (!enemy_context_generic)
     if (!enemy_context_generic)
     {
     {
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
         FURI_LOG_E("Game", "Failed to allocate EntityContext");
@@ -651,6 +808,16 @@ void spawn_enemy(Level *level, GameManager *manager, FuriString *json)
         return;
         return;
     }
     }
 
 
+    FuriString *is_user = get_json_value_furi("is_user", json);
+    bool is_user_value = false;
+    if (is_user)
+    {
+        is_user_value = strstr(furi_string_get_cstr(is_user), "true") != NULL;
+    }
+
+    FuriString *username = get_json_value_furi("username", json);
+    // no need to check for username, it is optional
+
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
     if (game_context && game_context->enemy_count < MAX_ENEMIES && !game_context->enemies[game_context->enemy_count])
     if (game_context && game_context->enemy_count < MAX_ENEMIES && !game_context->enemies[game_context->enemy_count])
     {
     {
@@ -664,7 +831,8 @@ void spawn_enemy(Level *level, GameManager *manager, FuriString *json)
                                                                                        atof_furi(speed),
                                                                                        atof_furi(speed),
                                                                                        atof_furi(attack_timer),
                                                                                        atof_furi(attack_timer),
                                                                                        atof_furi(strength),
                                                                                        atof_furi(strength),
-                                                                                       atof_furi(health)));
+                                                                                       atof_furi(health),
+                                                                                       is_user_value, username));
         game_context->enemy_count++;
         game_context->enemy_count++;
     }
     }
 
 
@@ -681,4 +849,12 @@ void spawn_enemy(Level *level, GameManager *manager, FuriString *json)
     furi_string_free(attack_timer);
     furi_string_free(attack_timer);
     furi_string_free(strength);
     furi_string_free(strength);
     furi_string_free(health);
     furi_string_free(health);
+    if (is_user)
+    {
+        furi_string_free(is_user);
+    }
+    if (username)
+    {
+        furi_string_free(username);
+    }
 }
 }

+ 58 - 6
game/game.c

@@ -11,10 +11,19 @@
 static void game_start(GameManager *game_manager, void *ctx)
 static void game_start(GameManager *game_manager, void *ctx)
 {
 {
     // Do some initialization here, for example you can load score from storage.
     // Do some initialization here, for example you can load score from storage.
+    // check if enough memory
+    if (!is_enough_heap(sizeof(GameContext), true))
+    {
+        FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
+        GameContext *game_context = ctx;
+        game_context->ended_early = true;
+        game_manager_game_stop(game_manager); // end game early
+        return;
+    }
     // For simplicity, we will just set it to 0.
     // For simplicity, we will just set it to 0.
     GameContext *game_context = ctx;
     GameContext *game_context = ctx;
     game_context->fps = atof_(fps_choices_str[fps_index]);
     game_context->fps = atof_(fps_choices_str[fps_index]);
-    game_context->player_context = NULL;
+    game_context->player = NULL;
     game_context->ended_early = false;
     game_context->ended_early = false;
     game_context->current_level = 0;
     game_context->current_level = 0;
     game_context->level_count = 0;
     game_context->level_count = 0;
@@ -61,13 +70,35 @@ static void game_start(GameManager *game_manager, void *ctx)
     }
     }
     else if (game_context->game_mode == GAME_MODE_PVP)
     else if (game_context->game_mode == GAME_MODE_PVP)
     {
     {
-        // show pvp menu
-        easy_flipper_dialog("Unavailable", "\nPvP mode is not ready yet.\nPress BACK to return.");
+        // show pvp
+        game_context->levels[0] = game_manager_add_level(game_manager, pvp_world());
+        game_context->level_count = 1;
     }
     }
 
 
     // imu
     // imu
     game_context->imu = imu_alloc();
     game_context->imu = imu_alloc();
     game_context->imu_present = imu_present(game_context->imu);
     game_context->imu_present = imu_present(game_context->imu);
+
+    // FlipperHTTP
+    if (game_context->game_mode == GAME_MODE_PVP)
+    {
+        // check if enough memory
+        if (!is_enough_heap(sizeof(FlipperHTTP), true))
+        {
+            FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
+            game_context->ended_early = true;
+            game_manager_game_stop(game_manager); // end game early
+            return;
+        }
+        game_context->fhttp = flipper_http_alloc();
+        if (!game_context->fhttp)
+        {
+            FURI_LOG_E("Game", "Failed to allocate FlipperHTTP");
+            game_context->ended_early = true;
+            game_manager_game_stop(game_manager); // end game early
+            return;
+        }
+    }
 }
 }
 
 
 static void thanks(Canvas *canvas, void *context)
 static void thanks(Canvas *canvas, void *context)
@@ -89,6 +120,7 @@ static void game_stop(void *ctx)
 {
 {
     furi_check(ctx);
     furi_check(ctx);
     GameContext *game_context = ctx;
     GameContext *game_context = ctx;
+    const size_t heap_size = memmgr_heap_get_max_free_block();
     imu_free(game_context->imu);
     imu_free(game_context->imu);
     game_context->imu = NULL;
     game_context->imu = NULL;
 
 
@@ -98,6 +130,16 @@ static void game_stop(void *ctx)
         level_clear(game_context->levels[game_context->current_level]);
         level_clear(game_context->levels[game_context->current_level]);
     }
     }
 
 
+    if (game_context->game_mode == GAME_MODE_PVP)
+    {
+        if (game_context->fhttp)
+        {
+            flipper_http_websocket_stop(game_context->fhttp); // close websocket
+            remove_player_from_lobby(game_context->fhttp);    // remove player from lobby
+            flipper_http_free(game_context->fhttp);
+        }
+    }
+
     PlayerContext *player_context = malloc(sizeof(PlayerContext));
     PlayerContext *player_context = malloc(sizeof(PlayerContext));
     if (!player_context)
     if (!player_context)
     {
     {
@@ -110,9 +152,13 @@ static void game_stop(void *ctx)
             "Game Over",
             "Game Over",
             "Thanks for playing FlipWorld!\nHit BACK then wait for\nthe game to save.");
             "Thanks for playing FlipWorld!\nHit BACK then wait for\nthe game to save.");
     else
     else
-        easy_flipper_dialog(
-            "Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit.");
+    {
+        char message[128];
+        snprintf(message, sizeof(message), "Ran out of memory so the\ngame ended early. There were\n%zu bytes free.\n\nHit BACK to exit.", heap_size);
+        easy_flipper_dialog("Game Over", message);
+    }
 
 
+    // save the player context
     if (load_player_context(player_context))
     if (load_player_context(player_context))
     {
     {
         ViewPort *view_port = view_port_alloc();
         ViewPort *view_port = view_port_alloc();
@@ -122,7 +168,13 @@ static void game_stop(void *ctx)
         uint32_t tick_count = furi_get_tick();
         uint32_t tick_count = furi_get_tick();
         furi_delay_ms(800);
         furi_delay_ms(800);
 
 
-        save_player_context_api(player_context);
+        // save the player context to the API
+        game_context->fhttp = flipper_http_alloc();
+        if (game_context->fhttp)
+        {
+            save_player_context_api(player_context, game_context->fhttp);
+            flipper_http_free(game_context->fhttp);
+        }
 
 
         const uint32_t delay = 3500;
         const uint32_t delay = 3500;
         tick_count = (tick_count + delay) - furi_get_tick();
         tick_count = (tick_count + delay) - furi_get_tick();

+ 27 - 85
game/icon.c

@@ -75,18 +75,10 @@ static void icon_start(Entity *self, GameManager *manager, void *context)
     free(loaded_data);
     free(loaded_data);
 }
 }
 
 
-// -------------- Stop callback --------------
-static void icon_free(Entity *self, GameManager *manager, void *context)
-{
-    UNUSED(self);
-    UNUSED(manager);
-    UNUSED(context);
-}
-
 // -------------- Entity description --------------
 // -------------- Entity description --------------
 const EntityDescription icon_desc = {
 const EntityDescription icon_desc = {
     .start = icon_start,
     .start = icon_start,
-    .stop = icon_free,
+    .stop = NULL,
     .update = NULL,
     .update = NULL,
     .render = icon_render,
     .render = icon_render,
     .collision = icon_collision,
     .collision = icon_collision,
@@ -94,7 +86,7 @@ const EntityDescription icon_desc = {
     .context_size = sizeof(IconContext),
     .context_size = sizeof(IconContext),
 };
 };
 
 
-static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t width, uint8_t height)
+static IconContext *icon_generic_alloc(IconID id, const Icon *icon, uint8_t width, uint8_t height)
 {
 {
     IconContext *ctx = malloc(sizeof(IconContext));
     IconContext *ctx = malloc(sizeof(IconContext));
     if (!ctx)
     if (!ctx)
@@ -102,7 +94,7 @@ static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t
         FURI_LOG_E("Game", "Failed to allocate IconContext");
         FURI_LOG_E("Game", "Failed to allocate IconContext");
         return NULL;
         return NULL;
     }
     }
-    snprintf(ctx->id, sizeof(ctx->id), "%s", id);
+    ctx->id = id;
     ctx->icon = icon;
     ctx->icon = icon;
     ctx->size = (Vector){width, height};
     ctx->size = (Vector){width, height};
     return ctx;
     return ctx;
@@ -111,99 +103,49 @@ static IconContext *icon_generic_alloc(const char *id, const Icon *icon, uint8_t
 IconContext *get_icon_context(const char *name)
 IconContext *get_icon_context(const char *name)
 {
 {
     if (is_str(name, "house"))
     if (is_str(name, "house"))
-        return icon_generic_alloc("house", &I_icon_house_48x32px, 48, 32);
+        return icon_generic_alloc(ICON_ID_HOUSE, &I_icon_house_48x32px, 48, 32);
     else if (is_str(name, "man"))
     else if (is_str(name, "man"))
-        return icon_generic_alloc("man", &I_icon_man_7x16, 7, 16);
+        return icon_generic_alloc(ICON_ID_MAN, &I_icon_man_7x16, 7, 16);
     else if (is_str(name, "plant"))
     else if (is_str(name, "plant"))
-        return icon_generic_alloc("plant", &I_icon_plant_16x16, 16, 16);
+        return icon_generic_alloc(ICON_ID_PLANT, &I_icon_plant_16x16, 16, 16);
     else if (is_str(name, "tree"))
     else if (is_str(name, "tree"))
-        return icon_generic_alloc("tree", &I_icon_tree_16x16, 16, 16);
+        return icon_generic_alloc(ICON_ID_TREE, &I_icon_tree_16x16, 16, 16);
     else if (is_str(name, "woman"))
     else if (is_str(name, "woman"))
-        return icon_generic_alloc("woman", &I_icon_woman_9x16, 9, 16);
+        return icon_generic_alloc(ICON_ID_WOMAN, &I_icon_woman_9x16, 9, 16);
     else if (is_str(name, "fence"))
     else if (is_str(name, "fence"))
-        return icon_generic_alloc("fence", &I_icon_fence_16x8px, 16, 8);
+        return icon_generic_alloc(ICON_ID_FENCE, &I_icon_fence_16x8px, 16, 8);
     else if (is_str(name, "fence_end"))
     else if (is_str(name, "fence_end"))
-        return icon_generic_alloc("fence_end", &I_icon_fence_end_16x8px, 16, 8);
-    // else if (is_str(name, "fence_vertical_end") )
-    //     return icon_generic_alloc("fence_vertical_end", &I_icon_fence_vertical_end_6x8px, 6, 8);
-    // else if (is_str(name, "fence_vertical_start") )
-    //     return icon_generic_alloc("fence_vertical_start", &I_icon_fence_vertical_start_6x15px, 6, 15);
+        return icon_generic_alloc(ICON_ID_FENCE_END, &I_icon_fence_end_16x8px, 16, 8);
+    else if (is_str(name, "fence_vertical_end"))
+        return icon_generic_alloc(ICON_ID_FENCE_VERTICAL_END, &I_icon_fence_vertical_end_6x8px, 6, 8);
+    else if (is_str(name, "fence_vertical_start"))
+        return icon_generic_alloc(ICON_ID_FENCE_VERTICAL_START, &I_icon_fence_vertical_start_6x15px, 6, 15);
     else if (is_str(name, "flower"))
     else if (is_str(name, "flower"))
-        return icon_generic_alloc("flower", &I_icon_flower_16x16, 16, 16);
+        return icon_generic_alloc(ICON_ID_FLOWER, &I_icon_flower_16x16, 16, 16);
     else if (is_str(name, "lake_bottom"))
     else if (is_str(name, "lake_bottom"))
-        return icon_generic_alloc("lake_bottom", &I_icon_lake_bottom_31x12px, 31, 12);
+        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM, &I_icon_lake_bottom_31x12px, 31, 12);
     else if (is_str(name, "lake_bottom_left"))
     else if (is_str(name, "lake_bottom_left"))
-        return icon_generic_alloc("lake_bottom_left", &I_icon_lake_bottom_left_24x22px, 24, 22);
+        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM_LEFT, &I_icon_lake_bottom_left_24x22px, 24, 22);
     else if (is_str(name, "lake_bottom_right"))
     else if (is_str(name, "lake_bottom_right"))
-        return icon_generic_alloc("lake_bottom_right", &I_icon_lake_bottom_right_24x22px, 24, 22);
+        return icon_generic_alloc(ICON_ID_LAKE_BOTTOM_RIGHT, &I_icon_lake_bottom_right_24x22px, 24, 22);
     else if (is_str(name, "lake_left"))
     else if (is_str(name, "lake_left"))
-        return icon_generic_alloc("lake_left", &I_icon_lake_left_11x31px, 11, 31);
+        return icon_generic_alloc(ICON_ID_LAKE_LEFT, &I_icon_lake_left_11x31px, 11, 31);
     else if (is_str(name, "lake_right"))
     else if (is_str(name, "lake_right"))
-        return icon_generic_alloc("lake_right", &I_icon_lake_right_11x31, 11, 31);
+        return icon_generic_alloc(ICON_ID_LAKE_RIGHT, &I_icon_lake_right_11x31, 11, 31);
     else if (is_str(name, "lake_top"))
     else if (is_str(name, "lake_top"))
-        return icon_generic_alloc("lake_top", &I_icon_lake_top_31x12px, 31, 12);
+        return icon_generic_alloc(ICON_ID_LAKE_TOP, &I_icon_lake_top_31x12px, 31, 12);
     else if (is_str(name, "lake_top_left"))
     else if (is_str(name, "lake_top_left"))
-        return icon_generic_alloc("lake_top_left", &I_icon_lake_top_left_24x22px, 24, 22);
+        return icon_generic_alloc(ICON_ID_LAKE_TOP_LEFT, &I_icon_lake_top_left_24x22px, 24, 22);
     else if (is_str(name, "lake_top_right"))
     else if (is_str(name, "lake_top_right"))
-        return icon_generic_alloc("lake_top_right", &I_icon_lake_top_right_24x22px, 24, 22);
+        return icon_generic_alloc(ICON_ID_LAKE_TOP_RIGHT, &I_icon_lake_top_right_24x22px, 24, 22);
     else if (is_str(name, "rock_large"))
     else if (is_str(name, "rock_large"))
-        return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
+        return icon_generic_alloc(ICON_ID_ROCK_LARGE, &I_icon_rock_large_18x19px, 18, 19);
     else if (is_str(name, "rock_medium"))
     else if (is_str(name, "rock_medium"))
-        return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
+        return icon_generic_alloc(ICON_ID_ROCK_MEDIUM, &I_icon_rock_medium_16x14px, 16, 14);
     else if (is_str(name, "rock_small"))
     else if (is_str(name, "rock_small"))
-        return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8);
+        return icon_generic_alloc(ICON_ID_ROCK_SMALL, &I_icon_rock_small_10x8px, 10, 8);
 
 
     // If no match is found
     // If no match is found
     FURI_LOG_E("Game", "Icon not found: %s", name);
     FURI_LOG_E("Game", "Icon not found: %s", name);
     return NULL;
     return NULL;
-}
-
-const char *icon_get_id(const Icon *icon)
-{
-    if (icon == &I_icon_house_48x32px)
-        return "house";
-    else if (icon == &I_icon_man_7x16)
-        return "man";
-    else if (icon == &I_icon_plant_16x16)
-        return "plant";
-    else if (icon == &I_icon_tree_16x16)
-        return "tree";
-    else if (icon == &I_icon_woman_9x16)
-        return "woman";
-    else if (icon == &I_icon_fence_16x8px)
-        return "fence";
-    else if (icon == &I_icon_fence_end_16x8px)
-        return "fence_end";
-    // else if (icon == &I_icon_fence_vertical_end_6x8px)
-    //     return "fence_vertical_end";
-    // else if (icon == &I_icon_fence_vertical_start_6x15px)
-    //     return "fence_vertical_start";
-    else if (icon == &I_icon_flower_16x16)
-        return "flower";
-    else if (icon == &I_icon_lake_bottom_31x12px)
-        return "lake_bottom";
-    else if (icon == &I_icon_lake_bottom_left_24x22px)
-        return "lake_bottom_left";
-    else if (icon == &I_icon_lake_bottom_right_24x22px)
-        return "lake_bottom_right";
-    else if (icon == &I_icon_lake_left_11x31px)
-        return "lake_left";
-    else if (icon == &I_icon_lake_right_11x31)
-        return "lake_right";
-    else if (icon == &I_icon_lake_top_31x12px)
-        return "lake_top";
-    else if (icon == &I_icon_lake_top_left_24x22px)
-        return "lake_top_left";
-    else if (icon == &I_icon_lake_top_right_24x22px)
-        return "lake_top_right";
-    else if (icon == &I_icon_rock_large_18x19px)
-        return "rock_large";
-    else if (icon == &I_icon_rock_medium_16x14px)
-        return "rock_medium";
-    else if (icon == &I_icon_rock_small_10x8px)
-        return "rock_small";
-
-    // If no match is found
-    FURI_LOG_E("Game", "Icon ID not found for given icon pointer.");
-    return NULL;
-}
+}

+ 27 - 3
game/icon.h

@@ -2,13 +2,37 @@
 #include "flip_world_icons.h"
 #include "flip_world_icons.h"
 #include "game.h"
 #include "game.h"
 
 
+typedef enum
+{
+    ICON_ID_HOUSE,                // House
+    ICON_ID_MAN,                  // Man
+    ICON_ID_PLANT,                // Plant
+    ICON_ID_TREE,                 // Tree
+    ICON_ID_WOMAN,                // Woman
+    ICON_ID_FENCE,                // Fence
+    ICON_ID_FENCE_END,            // Fence end
+    ICON_ID_FENCE_VERTICAL_END,   // Vertical fence end
+    ICON_ID_FENCE_VERTICAL_START, // Vertical fence start
+    ICON_ID_FLOWER,               // Flower
+    ICON_ID_LAKE_BOTTOM,          // Lake bottom
+    ICON_ID_LAKE_BOTTOM_LEFT,     // Lake bottom left
+    ICON_ID_LAKE_BOTTOM_RIGHT,    // Lake bottom right
+    ICON_ID_LAKE_LEFT,            // Lake left
+    ICON_ID_LAKE_RIGHT,           // Lake right
+    ICON_ID_LAKE_TOP,             // Lake top
+    ICON_ID_LAKE_TOP_LEFT,        // Lake top left
+    ICON_ID_LAKE_TOP_RIGHT,       // Lake top right
+    ICON_ID_ROCK_LARGE,           // Large rock
+    ICON_ID_ROCK_MEDIUM,          // Medium rock
+    ICON_ID_ROCK_SMALL,           // Small rock
+} IconID;
+
 typedef struct
 typedef struct
 {
 {
-    char id[32];
+    IconID id;
     const Icon *icon;
     const Icon *icon;
     Vector size;
     Vector size;
 } IconContext;
 } IconContext;
 
 
 extern const EntityDescription icon_desc;
 extern const EntityDescription icon_desc;
-IconContext *get_icon_context(const char *name);
-const char *icon_get_id(const Icon *icon);
+IconContext *get_icon_context(const char *name);

+ 7 - 5
game/level.c

@@ -40,11 +40,12 @@ void set_world(Level *level, GameManager *manager, char *id)
     if (!json_data_str || furi_string_empty(json_data_str))
     if (!json_data_str || furi_string_empty(json_data_str))
     {
     {
         FURI_LOG_E("Game", "Failed to load json data from file");
         FURI_LOG_E("Game", "Failed to load json data from file");
-        // draw_town_world(manager, level);
+        if (json_data_str)
+            furi_string_free(json_data_str);
         return;
         return;
     }
     }
 
 
-    if (!is_enough_heap(28400))
+    if (!is_enough_heap(28400, true))
     {
     {
         FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
         FURI_LOG_E("Game", "Not enough heap memory.. ending game early.");
         GameContext *game_context = game_manager_game_context_get(manager);
         GameContext *game_context = game_manager_game_context_get(manager);
@@ -58,7 +59,6 @@ void set_world(Level *level, GameManager *manager, char *id)
     if (!draw_json_world_furi(manager, level, json_data_str))
     if (!draw_json_world_furi(manager, level, json_data_str))
     {
     {
         FURI_LOG_E("Game", "Failed to draw world");
         FURI_LOG_E("Game", "Failed to draw world");
-        // draw_town_world(manager, level);
         furi_string_free(json_data_str);
         furi_string_free(json_data_str);
     }
     }
     else
     else
@@ -73,7 +73,8 @@ void set_world(Level *level, GameManager *manager, char *id)
         if (!enemy_data_str || furi_string_empty(enemy_data_str))
         if (!enemy_data_str || furi_string_empty(enemy_data_str))
         {
         {
             FURI_LOG_E("Game", "Failed to get enemy data");
             FURI_LOG_E("Game", "Failed to get enemy data");
-            // draw_town_world(manager, level);
+            if (enemy_data_str)
+                furi_string_free(enemy_data_str);
             return;
             return;
         }
         }
 
 
@@ -104,7 +105,8 @@ void set_world(Level *level, GameManager *manager, char *id)
         if (!npc_data_str || furi_string_empty(npc_data_str))
         if (!npc_data_str || furi_string_empty(npc_data_str))
         {
         {
             FURI_LOG_E("Game", "Failed to get npc data");
             FURI_LOG_E("Game", "Failed to get npc data");
-            // draw_town_world(manager, level);
+            if (npc_data_str)
+                furi_string_free(npc_data_str);
             return;
             return;
         }
         }
 
 

+ 1 - 1
game/level.h

@@ -3,7 +3,7 @@
 #include "flip_world.h"
 #include "flip_world.h"
 typedef struct
 typedef struct
 {
 {
-    char id[64];
+    char id[32];
     int index;
     int index;
 } LevelContext;
 } LevelContext;
 
 

+ 9 - 7
game/npc.c

@@ -135,8 +135,10 @@ static void npc_collision(Entity *self, Entity *other, GameManager *manager, voi
         // Retrieve NPC context
         // Retrieve NPC context
         EntityContext *npc_context = (EntityContext *)context;
         EntityContext *npc_context = (EntityContext *)context;
         GameContext *game_context = game_manager_game_context_get(manager);
         GameContext *game_context = game_manager_game_context_get(manager);
+        PlayerContext *player_context = entity_context_get(game_context->player);
         furi_check(npc_context);
         furi_check(npc_context);
         furi_check(game_context);
         furi_check(game_context);
+        furi_check(player_context);
 
 
         // Get positions of the NPC and the player
         // Get positions of the NPC and the player
         Vector npc_pos = entity_pos_get(self);
         Vector npc_pos = entity_pos_get(self);
@@ -146,20 +148,20 @@ static void npc_collision(Entity *self, Entity *other, GameManager *manager, voi
         bool player_is_facing_npc = false;
         bool player_is_facing_npc = false;
 
 
         // Determine if the player is facing the NPC
         // Determine if the player is facing the NPC
-        if ((game_context->player_context->direction == ENTITY_LEFT && npc_pos.x < player_pos.x) ||
-            (game_context->player_context->direction == ENTITY_RIGHT && npc_pos.x > player_pos.x) ||
-            (game_context->player_context->direction == ENTITY_UP && npc_pos.y < player_pos.y) ||
-            (game_context->player_context->direction == ENTITY_DOWN && npc_pos.y > player_pos.y))
+        if ((player_context->direction == ENTITY_LEFT && npc_pos.x < player_pos.x) ||
+            (player_context->direction == ENTITY_RIGHT && npc_pos.x > player_pos.x) ||
+            (player_context->direction == ENTITY_UP && npc_pos.y < player_pos.y) ||
+            (player_context->direction == ENTITY_DOWN && npc_pos.y > player_pos.y))
         {
         {
             player_is_facing_npc = true;
             player_is_facing_npc = true;
         }
         }
 
 
         // bounce the player back to where it came from
         // bounce the player back to where it came from
         // Set the player's old position to prevent collision
         // Set the player's old position to prevent collision
-        entity_pos_set(other, game_context->player_context->old_position);
+        entity_pos_set(other, player_context->old_position);
         // Reset player's movement direction to prevent immediate re-collision
         // Reset player's movement direction to prevent immediate re-collision
-        game_context->player_context->dx = 0;
-        game_context->player_context->dy = 0;
+        player_context->dx = 0;
+        player_context->dy = 0;
 
 
         // Press OK and facing NPC
         // Press OK and facing NPC
         if (player_is_facing_npc && game_context->last_button == GameKeyOk)
         if (player_is_facing_npc && game_context->last_button == GameKeyOk)

+ 33 - 8
game/player.c

@@ -143,9 +143,6 @@ void player_spawn(Level *level, GameManager *manager)
                 snprintf(pctx->username, sizeof(pctx->username), "Player");
                 snprintf(pctx->username, sizeof(pctx->username), "Player");
             }
             }
         }
         }
-
-        game_context->player_context = pctx;
-
         // Save the initialized context
         // Save the initialized context
         if (!save_player_context(pctx))
         if (!save_player_context(pctx))
         {
         {
@@ -171,8 +168,6 @@ void player_spawn(Level *level, GameManager *manager)
     // set the player's left sprite direction
     // set the player's left sprite direction
     pctx->left = pctx->direction == ENTITY_LEFT ? true : false;
     pctx->left = pctx->direction == ENTITY_LEFT ? true : false;
 
 
-    // Assign loaded player context to game context
-    game_context->player_context = pctx;
     free(sprite_context);
     free(sprite_context);
 }
 }
 
 
@@ -213,7 +208,7 @@ static void vgm_direction(Imu *imu, PlayerContext *player, Vector *pos)
         player->direction = ENTITY_UP;
         player->direction = ENTITY_UP;
     }
     }
 }
 }
-
+uint16_t elapsed_ws_timer = 0;
 static void player_update(Entity *self, GameManager *manager, void *context)
 static void player_update(Entity *self, GameManager *manager, void *context)
 {
 {
     if (!self || !manager || !context)
     if (!self || !manager || !context)
@@ -222,9 +217,39 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     PlayerContext *player = (PlayerContext *)context;
     PlayerContext *player = (PlayerContext *)context;
     InputState input = game_manager_input_get(manager);
     InputState input = game_manager_input_get(manager);
     Vector pos = entity_pos_get(self);
     Vector pos = entity_pos_get(self);
-    player->old_position = pos;
     GameContext *game_context = game_manager_game_context_get(manager);
     GameContext *game_context = game_manager_game_context_get(manager);
 
 
+    // update websocket player context
+    if (game_context->game_mode == GAME_MODE_PVP)
+    {
+        // if pvp, end the game if the player is dead
+        if (player->health <= 0)
+        {
+            player->health = player->max_health;
+            save_player_context(player);
+            furi_delay_ms(100);
+            game_manager_game_stop(manager);
+            return;
+        }
+
+        if (player->old_position.x != pos.x || player->old_position.y != pos.y)
+        {
+            elapsed_ws_timer++;
+            // only send the websocket update every 200ms
+            if (elapsed_ws_timer >= (game_context->fps / 5))
+            {
+                if (game_context->fhttp)
+                {
+                    player->start_position = player->old_position;
+                    websocket_player_context(player, game_context->fhttp);
+                }
+                elapsed_ws_timer = 0;
+            }
+        }
+    }
+
+    player->old_position = pos;
+
     // Determine the player's level based on XP
     // Determine the player's level based on XP
     player->level = get_player_level_iterative(player->xp);
     player->level = get_player_level_iterative(player->xp);
     player->strength = 10 + (player->level * 1);           // 1 strength per level
     player->strength = 10 + (player->level * 1);           // 1 strength per level
@@ -357,7 +382,7 @@ static void player_update(Entity *self, GameManager *manager, void *context)
 
 
         // if all enemies are dead, allow the "OK" button to switch levels
         // if all enemies are dead, allow the "OK" button to switch levels
         // otherwise the "OK" button will be used to attack
         // otherwise the "OK" button will be used to attack
-        if (game_context->enemy_count == 0 && !game_context->is_switching_level)
+        if (game_context->game_mode != GAME_MODE_PVP && game_context->enemy_count == 0 && !game_context->is_switching_level)
         {
         {
             game_context->is_switching_level = true;
             game_context->is_switching_level = true;
             save_player_context(player);
             save_player_context(player);

+ 19 - 16
game/player.h

@@ -4,15 +4,15 @@
 #include <game/game.h>
 #include <game/game.h>
 #include "engine/sensors/imu.h"
 #include "engine/sensors/imu.h"
 
 
-#define MAX_ENEMIES 10
-#define MAX_LEVELS 10
-#define MAX_NPCS 10
+#define MAX_ENEMIES 5
+#define MAX_LEVELS 5
+#define MAX_NPCS 1
 
 
 // EntityContext definition
 // EntityContext definition
 typedef struct
 typedef struct
 {
 {
-    char id[64];                // Unique ID for the entity type
-    int index;                  // Index for the specific entity instance
+    char id[32];                // Unique ID for the entity type
+    uint8_t index;              // Index for the specific entity instance
     Vector size;                // Size of the entity
     Vector size;                // Size of the entity
     Sprite *sprite_right;       // Entity sprite when looking right
     Sprite *sprite_right;       // Entity sprite when looking right
     Sprite *sprite_left;        // Entity sprite when looking left
     Sprite *sprite_left;        // Entity sprite when looking left
@@ -26,9 +26,11 @@ typedef struct
     float speed;                // Speed of the entity
     float speed;                // Speed of the entity
     float attack_timer;         // Cooldown duration between attacks
     float attack_timer;         // Cooldown duration between attacks
     float elapsed_attack_timer; // Time elapsed since the last attack
     float elapsed_attack_timer; // Time elapsed since the last attack
-    float strength;             // Damage the entity deals
+    uint32_t strength;          // Damage the entity deals
     float health;               // Health of the entity
     float health;               // Health of the entity
     char message[64];           // Message to display when interacting with the entity
     char message[64];           // Message to display when interacting with the entity
+    bool is_user;               // Flag to indicate if the entity is a live player or not
+    char username[32];          // entity username
 } EntityContext;
 } EntityContext;
 
 
 typedef struct
 typedef struct
@@ -46,7 +48,7 @@ typedef struct
     uint32_t strength;          // player strength
     uint32_t strength;          // player strength
     uint32_t health;            // player health
     uint32_t health;            // player health
     uint32_t max_health;        // player maximum health
     uint32_t max_health;        // player maximum health
-    uint32_t health_regen;      // player health regeneration rate per second/frame
+    uint8_t health_regen;       // player health regeneration rate per second/frame
     float elapsed_health_regen; // time elapsed since last health regeneration
     float elapsed_health_regen; // time elapsed since last health regeneration
     float attack_timer;         // Cooldown duration between attacks
     float attack_timer;         // Cooldown duration between attacks
     float elapsed_attack_timer; // Time elapsed since the last attack
     float elapsed_attack_timer; // Time elapsed since the last attack
@@ -72,17 +74,16 @@ typedef enum
 
 
 typedef struct
 typedef struct
 {
 {
-    PlayerContext *player_context;
     Level *levels[MAX_LEVELS];
     Level *levels[MAX_LEVELS];
     Entity *enemies[MAX_ENEMIES];
     Entity *enemies[MAX_ENEMIES];
     Entity *npcs[MAX_NPCS];
     Entity *npcs[MAX_NPCS];
     Entity *player;
     Entity *player;
     //
     //
     float fps;
     float fps;
-    int level_count;
-    int enemy_count;
-    int npc_count;
-    int current_level;
+    int8_t level_count;
+    int8_t enemy_count;
+    int8_t npc_count;
+    int8_t current_level;
     bool ended_early;
     bool ended_early;
     Imu *imu;
     Imu *imu;
     bool imu_present;
     bool imu_present;
@@ -90,20 +91,22 @@ typedef struct
     bool is_switching_level;
     bool is_switching_level;
     bool is_menu_open;
     bool is_menu_open;
     //
     //
-    uint32_t elapsed_button_timer;
-    uint32_t last_button;
+    uint16_t elapsed_button_timer;
+    uint8_t last_button;
     //
     //
     GameMenuScreen menu_screen;
     GameMenuScreen menu_screen;
     uint8_t menu_selection;
     uint8_t menu_selection;
     //
     //
     GameMode game_mode;
     GameMode game_mode;
     //
     //
-    int icon_count;
-    int icon_offset;
+    uint32_t icon_count;
+    uint16_t icon_offset;
     //
     //
     char message[64];
     char message[64];
     //
     //
     uint8_t tutorial_step;
     uint8_t tutorial_step;
+    //
+    FlipperHTTP *fhttp;
 } GameContext;
 } GameContext;
 
 
 typedef struct
 typedef struct

+ 292 - 144
game/storage.c

@@ -203,183 +203,329 @@ bool save_player_context(PlayerContext *player_context)
     return true;
     return true;
 }
 }
 
 
-bool save_player_context_api(PlayerContext *player_context)
+static FuriString *player_context_json(PlayerContext *player_context, bool websocket)
 {
 {
-    if (!player_context)
-    {
-        FURI_LOG_E(TAG, "Invalid player context");
-        return false;
-    }
-
-    FlipperHTTP *fhttp = flipper_http_alloc();
-    if (!fhttp)
-    {
-        FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
-        return false;
-    }
-
-    // create JSON for all the player context data
     FuriString *json = furi_string_alloc();
     FuriString *json = furi_string_alloc();
     if (!json)
     if (!json)
     {
     {
         FURI_LOG_E(TAG, "Failed to allocate JSON string");
         FURI_LOG_E(TAG, "Failed to allocate JSON string");
-        return false;
+        return NULL;
     }
     }
 
 
-    // opening brace
     furi_string_cat_str(json, "{");
     furi_string_cat_str(json, "{");
 
 
-    // 1. Username (String)
-    furi_string_cat_str(json, "\"username\":\"");
-    furi_string_cat_str(json, player_context->username);
-    furi_string_cat_str(json, "\",");
+    if (websocket)
+    {
+        // Minimal JSON for WebSocket (abbreviated, <128 characters)
+        // "u": username
+        furi_string_cat_str(json, "\"u\":\"");
+        furi_string_cat_str(json, player_context->username);
+        furi_string_cat_str(json, "\",");
+
+        // "xp": experience
+        furi_string_cat_str(json, "\"xp\":");
+        char buffer[32];
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->xp);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "h": health
+        furi_string_cat_str(json, "\"h\":");
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->health);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "ehr": elapsed health regen (1 decimal)
+        furi_string_cat_str(json, "\"ehr\":");
+        snprintf(buffer, sizeof(buffer), "%.1f", (double)player_context->elapsed_health_regen);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "eat": elapsed attack timer (1 decimal)
+        furi_string_cat_str(json, "\"eat\":");
+        snprintf(buffer, sizeof(buffer), "%.1f", (double)player_context->elapsed_attack_timer);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "d": direction (numeric code)
+        furi_string_cat_str(json, "\"d\":");
+        snprintf(buffer, sizeof(buffer), "%d", player_context->direction);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "s": state (numeric code)
+        furi_string_cat_str(json, "\"s\":");
+        snprintf(buffer, sizeof(buffer), "%d", player_context->state);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // "sp": start position object with x and y (1 decimal)
+        furi_string_cat_str(json, "\"sp\":{");
+        furi_string_cat_str(json, "\"x\":");
+        snprintf(buffer, sizeof(buffer), "%.1f", (double)player_context->start_position.x);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",\"y\":");
+        snprintf(buffer, sizeof(buffer), "%.1f", (double)player_context->start_position.y);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, "}");
+    }
+    else
+    {
+        // Full JSON output (unchanged)
+        // 1. Username
+        furi_string_cat_str(json, "\"username\":\"");
+        furi_string_cat_str(json, player_context->username);
+        furi_string_cat_str(json, "\",");
+
+        // 2. Level
+        furi_string_cat_str(json, "\"level\":");
+        char buffer[32];
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->level);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 3. XP
+        furi_string_cat_str(json, "\"xp\":");
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->xp);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 4. Health
+        furi_string_cat_str(json, "\"health\":");
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->health);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 5. Strength
+        furi_string_cat_str(json, "\"strength\":");
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->strength);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 6. Max Health
+        furi_string_cat_str(json, "\"max_health\":");
+        snprintf(buffer, sizeof(buffer), "%lu", player_context->max_health);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 7. Health Regen
+        furi_string_cat_str(json, "\"health_regen\":");
+        snprintf(buffer, sizeof(buffer), "%u", player_context->health_regen);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 8. Elapsed Health Regen
+        furi_string_cat_str(json, "\"elapsed_health_regen\":");
+        snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_health_regen);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 9. Attack Timer
+        furi_string_cat_str(json, "\"attack_timer\":");
+        snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->attack_timer);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 10. Elapsed Attack Timer
+        furi_string_cat_str(json, "\"elapsed_attack_timer\":");
+        snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_attack_timer);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
+
+        // 11. Direction (string representation)
+        furi_string_cat_str(json, "\"direction\":");
+        switch (player_context->direction)
+        {
+        case ENTITY_UP:
+            furi_string_cat_str(json, "\"up\",");
+            break;
+        case ENTITY_DOWN:
+            furi_string_cat_str(json, "\"down\",");
+            break;
+        case ENTITY_LEFT:
+            furi_string_cat_str(json, "\"left\",");
+            break;
+        case ENTITY_RIGHT:
+        default:
+            furi_string_cat_str(json, "\"right\",");
+            break;
+        }
 
 
-    // 2. Level (uint32_t)
-    furi_string_cat_str(json, "\"level\":");
-    char buffer[32];
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->level);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+        // 12. State (string representation)
+        furi_string_cat_str(json, "\"state\":");
+        switch (player_context->state)
+        {
+        case ENTITY_IDLE:
+            furi_string_cat_str(json, "\"idle\",");
+            break;
+        case ENTITY_MOVING:
+            furi_string_cat_str(json, "\"moving\",");
+            break;
+        case ENTITY_ATTACKING:
+            furi_string_cat_str(json, "\"attacking\",");
+            break;
+        case ENTITY_ATTACKED:
+            furi_string_cat_str(json, "\"attacked\",");
+            break;
+        case ENTITY_DEAD:
+            furi_string_cat_str(json, "\"dead\",");
+            break;
+        default:
+            furi_string_cat_str(json, "\"unknown\",");
+            break;
+        }
 
 
-    // 3. XP (uint32_t)
-    furi_string_cat_str(json, "\"xp\":");
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->xp);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+        // 13. Start Position X
+        furi_string_cat_str(json, "\"start_position_x\":");
+        snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.x);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
 
 
-    // 4. Health (uint32_t)
-    furi_string_cat_str(json, "\"health\":");
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->health);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+        // 14. Start Position Y
+        furi_string_cat_str(json, "\"start_position_y\":");
+        snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.y);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
 
 
-    // 5. Strength (uint32_t)
-    furi_string_cat_str(json, "\"strength\":");
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->strength);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+        // 15. dx
+        furi_string_cat_str(json, "\"dx\":");
+        snprintf(buffer, sizeof(buffer), "%d", player_context->dx);
+        furi_string_cat_str(json, buffer);
+        furi_string_cat_str(json, ",");
 
 
-    // 6. Max Health (uint32_t)
-    furi_string_cat_str(json, "\"max_health\":");
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->max_health);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+        // 16. dy
+        furi_string_cat_str(json, "\"dy\":");
+        snprintf(buffer, sizeof(buffer), "%d", player_context->dy);
+        furi_string_cat_str(json, buffer);
+    }
 
 
-    // 7. Health Regen (uint32_t)
-    furi_string_cat_str(json, "\"health_regen\":");
-    snprintf(buffer, sizeof(buffer), "%lu", player_context->health_regen);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    furi_string_cat_str(json, "}");
 
 
-    // 8. Elapsed Health Regen (float)
-    furi_string_cat_str(json, "\"elapsed_health_regen\":");
-    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_health_regen);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    // For websocket, output only the minimal JSON (without extra wrapping)
+    FuriString *json_data = furi_string_alloc();
+    if (!json_data)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate JSON string");
+        furi_string_free(json);
+        return NULL;
+    }
 
 
-    // 9. Attack Timer (float)
-    furi_string_cat_str(json, "\"attack_timer\":");
-    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->attack_timer);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    if (websocket)
+    {
+        furi_string_cat(json_data, json);
+    }
+    else
+    {
+        furi_string_cat_str(json_data, "{\"username\":\"");
+        furi_string_cat_str(json_data, player_context->username);
+        furi_string_cat_str(json_data, "\",\"game_stats\":");
+        furi_string_cat(json_data, json);
+        furi_string_cat_str(json_data, "}");
+    }
 
 
-    // 10. Elapsed Attack Timer (float)
-    furi_string_cat_str(json, "\"elapsed_attack_timer\":");
-    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_attack_timer);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    furi_string_free(json);
+    return json_data;
+}
 
 
-    // 11. Direction (enum PlayerDirection)
-    furi_string_cat_str(json, "\"direction\":");
-    switch (player_context->direction)
-    {
-    case ENTITY_UP:
-        furi_string_cat_str(json, "\"up\",");
-        break;
-    case ENTITY_DOWN:
-        furi_string_cat_str(json, "\"down\",");
-        break;
-    case ENTITY_LEFT:
-        furi_string_cat_str(json, "\"left\",");
-        break;
-    case ENTITY_RIGHT:
-    default:
-        furi_string_cat_str(json, "\"right\",");
-        break;
+bool save_player_context_api(PlayerContext *player_context, FlipperHTTP *fhttp)
+{
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Invalid player context");
+        return false;
     }
     }
 
 
-    // 12. State (enum PlayerState)
-    furi_string_cat_str(json, "\"state\":");
-    switch (player_context->state)
-    {
-    case ENTITY_IDLE:
-        furi_string_cat_str(json, "\"idle\",");
-        break;
-    case ENTITY_MOVING:
-        furi_string_cat_str(json, "\"moving\",");
-        break;
-    case ENTITY_ATTACKING:
-        furi_string_cat_str(json, "\"attacking\",");
-        break;
-    case ENTITY_ATTACKED:
-        furi_string_cat_str(json, "\"attacked\",");
-        break;
-    case ENTITY_DEAD:
-        furi_string_cat_str(json, "\"dead\",");
-        break;
-    default:
-        furi_string_cat_str(json, "\"unknown\",");
-        break;
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
+        return false;
     }
     }
 
 
-    // 13. Start Position X (float)
-    furi_string_cat_str(json, "\"start_position_x\":");
-    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.x);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
-
-    // 14. Start Position Y (float)
-    furi_string_cat_str(json, "\"start_position_y\":");
-    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.y);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    FuriString *json_data = player_context_json(player_context, false);
+    if (!json_data)
+    {
+        FURI_LOG_E(TAG, "Failed to create JSON data");
+        return false;
+    }
 
 
-    // 15. dx (int8_t)
-    furi_string_cat_str(json, "\"dx\":");
-    snprintf(buffer, sizeof(buffer), "%d", player_context->dx);
-    furi_string_cat_str(json, buffer);
-    furi_string_cat_str(json, ",");
+    // save the json_data to the API
+    if (!flipper_http_request(fhttp, POST, "https://www.jblanked.com/flipper/api/user/update-game-stats/", "{\"Content-Type\": \"application/json\"}", furi_string_get_cstr(json_data)))
+    {
+        FURI_LOG_E(TAG, "Failed to save player context to API");
+        furi_string_free(json_data);
+        return false;
+    }
+    fhttp->state = RECEIVING;
+    while (fhttp->state != IDLE)
+    {
+        furi_delay_ms(100);
+    }
+    furi_string_free(json_data);
 
 
-    // 16. dy (int8_t)
-    furi_string_cat_str(json, "\"dy\":");
-    snprintf(buffer, sizeof(buffer), "%d", player_context->dy);
-    furi_string_cat_str(json, buffer);
+    return true;
+}
 
 
-    // closing brace
-    furi_string_cat_str(json, "}");
+bool websocket_player_context(PlayerContext *player_context, FlipperHTTP *fhttp)
+{
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Invalid player context");
+        return false;
+    }
 
 
-    // create new JSON with username key (of just username), and game_stats key (of the all of the data)
-    FuriString *json_data = furi_string_alloc();
-    if (!json_data)
+    if (!fhttp)
     {
     {
-        FURI_LOG_E(TAG, "Failed to allocate JSON string");
-        furi_string_free(json);
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
         return false;
         return false;
     }
     }
 
 
-    furi_string_cat_str(json_data, "{\"username\":\"");
-    furi_string_cat_str(json_data, player_context->username);
-    furi_string_cat_str(json_data, "\",\"game_stats\":");
-    furi_string_cat(json_data, json);
-    furi_string_cat_str(json_data, "}");
+    // create JSON for all the player context data
+    FuriString *json = player_context_json(player_context, true);
+    if (!json)
+    {
+        FURI_LOG_E(TAG, "Failed to create JSON data");
+        return false;
+    }
 
 
+    // websocket session is already started, so just send json to esp32
+    if (!flipper_http_send_data(fhttp, furi_string_get_cstr(json)))
+    {
+        FURI_LOG_E(TAG, "Failed to send player context to websocket");
+        furi_string_free(json);
+        return false;
+    }
     furi_string_free(json);
     furi_string_free(json);
 
 
-    // save the json_data to the API
-    if (!flipper_http_request(fhttp, POST, "https://www.jblanked.com/flipper/api/user/update-game-stats/", "{\"Content-Type\": \"application/json\"}", furi_string_get_cstr(json_data)))
+    return true;
+}
+
+bool remove_player_from_lobby(FlipperHTTP *fhttp)
+{
+    if (!fhttp)
     {
     {
-        FURI_LOG_E(TAG, "Failed to save player context to API");
-        furi_string_free(json_data);
+        FURI_LOG_E(TAG, "FlipperHTTP is NULL");
+        return false;
+    }
+    char username[32];
+    if (!load_char("Flip-Social-Username", username, sizeof(username)))
+    {
+        FURI_LOG_E(TAG, "Failed to load data/Flip-Social-Username");
+        return false;
+    }
+    char lobby_name[32];
+    if (!load_char("pvp_lobby_name", lobby_name, sizeof(lobby_name)))
+    {
+        FURI_LOG_E(TAG, "Failed to load data/pvp_lobby_name");
+        return false;
+    }
+    char url[128];
+    char payload[128];
+    snprintf(payload, sizeof(payload), "{\"username\":\"%s\", \"game_id\":\"%s\"}", username, lobby_name);
+    snprintf(url, sizeof(url), "https://www.jblanked.com/flipper/api/world/pvp/lobby/remove/");
+    fhttp->state = IDLE;
+    if (!flipper_http_request(fhttp, POST, url, "{\"Content-Type\":\"application/json\"}", payload))
+    {
+        FURI_LOG_E(TAG, "Failed to remove player from lobby");
         return false;
         return false;
     }
     }
     fhttp->state = RECEIVING;
     fhttp->state = RECEIVING;
@@ -387,8 +533,6 @@ bool save_player_context_api(PlayerContext *player_context)
     {
     {
         furi_delay_ms(100);
         furi_delay_ms(100);
     }
     }
-    furi_string_free(json_data);
-    flipper_http_free(fhttp);
     return true;
     return true;
 }
 }
 
 
@@ -961,6 +1105,10 @@ bool separate_world_data(char *id, FuriString *world_data)
     if (!file_json_data || furi_string_size(file_json_data) == 0)
     if (!file_json_data || furi_string_size(file_json_data) == 0)
     {
     {
         FURI_LOG_E("Game", "Failed to get json data in separate_world_data");
         FURI_LOG_E("Game", "Failed to get json data in separate_world_data");
+        if (file_json_data)
+        {
+            furi_string_free(file_json_data);
+        }
         return false;
         return false;
     }
     }
 
 

+ 3 - 1
game/storage.h

@@ -5,7 +5,9 @@
 #include <flip_storage/storage.h>
 #include <flip_storage/storage.h>
 
 
 bool save_player_context(PlayerContext *player_context);
 bool save_player_context(PlayerContext *player_context);
-bool save_player_context_api(PlayerContext *player_context);
+bool save_player_context_api(PlayerContext *player_context, FlipperHTTP *fhttp);
+bool websocket_player_context(PlayerContext *player_context, FlipperHTTP *fhttp);
+bool remove_player_from_lobby(FlipperHTTP *fhttp);
 bool load_player_context(PlayerContext *player_context);
 bool load_player_context(PlayerContext *player_context);
 bool set_player_context();
 bool set_player_context();
 
 

+ 40 - 0
game/world.c

@@ -124,6 +124,46 @@ const LevelBehaviour *training_world()
     return &_training_world;
     return &_training_world;
 }
 }
 
 
+static void draw_pvp_world(Level *level, GameManager *manager, void *context)
+{
+    UNUSED(context);
+    if (!manager || !level)
+    {
+        FURI_LOG_E("Game", "Manager or level is NULL");
+        return;
+    }
+    GameContext *game_context = game_manager_game_context_get(manager);
+    level_clear(level);
+    FuriString *json_data_str = furi_string_alloc();
+    furi_string_cat_str(json_data_str, "{\"name\":\"pvp_world\",\"author\":\"ChatGPT\",\"json_data\":[{\"icon\":\"rock_medium\",\"x\":100,\"y\":100,\"amount\":10,\"horizontal\":true},{\"icon\":\"rock_medium\",\"x\":400,\"y\":300,\"amount\":6,\"horizontal\":true},{\"icon\":\"rock_small\",\"x\":600,\"y\":200,\"amount\":8,\"horizontal\":true},{\"icon\":\"fence\",\"x\":50,\"y\":50,\"amount\":10,\"horizontal\":true},{\"icon\":\"fence\",\"x\":250,\"y\":150,\"amount\":12,\"horizontal\":true},{\"icon\":\"fence\",\"x\":550,\"y\":350,\"amount\":12,\"horizontal\":true},{\"icon\":\"rock_large\",\"x\":400,\"y\":70,\"amount\":12,\"horizontal\":true},{\"icon\":\"rock_large\",\"x\":200,\"y\":200,\"amount\":6,\"horizontal\":false},{\"icon\":\"tree\",\"x\":5,\"y\":5,\"amount\":45,\"horizontal\":true},{\"icon\":\"tree\",\"x\":5,\"y\":5,\"amount\":20,\"horizontal\":false},{\"icon\":\"tree\",\"x\":22,\"y\":22,\"amount\":44,\"horizontal\":true},{\"icon\":\"tree\",\"x\":22,\"y\":22,\"amount\":20,\"horizontal\":false},{\"icon\":\"tree\",\"x\":5,\"y\":347,\"amount\":45,\"horizontal\":true},{\"icon\":\"tree\",\"x\":5,\"y\":364,\"amount\":45,\"horizontal\":true},{\"icon\":\"tree\",\"x\":735,\"y\":37,\"amount\":18,\"horizontal\":false},{\"icon\":\"tree\",\"x\":752,\"y\":37,\"amount\":18,\"horizontal\":false}]}");
+    if (!separate_world_data("pvp_world", json_data_str))
+    {
+        FURI_LOG_E("Game", "Failed to separate world data");
+    }
+    furi_string_free(json_data_str);
+    set_world(level, manager, "pvp_world");
+    game_context->is_switching_level = false;
+    game_context->icon_offset = 0;
+    if (!game_context->imu_present)
+    {
+        game_context->icon_offset += ((game_context->icon_count / 10) / 15);
+    }
+    player_spawn(level, manager);
+}
+
+static const LevelBehaviour _pvp_world = {
+    .alloc = NULL,
+    .free = NULL,
+    .start = draw_pvp_world,
+    .stop = NULL,
+    .context_size = 0,
+};
+
+const LevelBehaviour *pvp_world()
+{
+    return &_pvp_world;
+}
+
 FuriString *fetch_world(const char *name)
 FuriString *fetch_world(const char *name)
 {
 {
     if (!name)
     if (!name)

+ 1 - 0
game/world.h

@@ -11,5 +11,6 @@
 // Maximum number of world objects
 // Maximum number of world objects
 #define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds
 #define MAX_WORLD_OBJECTS 25 // any more than that and we may run out of heap when switching worlds
 const LevelBehaviour *training_world();
 const LevelBehaviour *training_world();
+const LevelBehaviour *pvp_world();
 bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
 bool draw_json_world_furi(GameManager *manager, Level *level, const FuriString *json_data);
 FuriString *fetch_world(const char *name);
 FuriString *fetch_world(const char *name);

+ 1 - 1
jsmn/jsmn_h.h

@@ -10,7 +10,7 @@
 
 
 #undef FURI_LOG_I
 #undef FURI_LOG_I
 #define FURI_LOG_I(tag, msg, ...)
 #define FURI_LOG_I(tag, msg, ...)
-//
+// //
 
 
 typedef enum
 typedef enum
 {
 {

Некоторые файлы не были показаны из-за большого количества измененных файлов