Explorar o código

Merge pull request #8 from jblanked/dev_0.1

FlipWorld - v01 (Official Release)
JBlanked hai 1 ano
pai
achega
f9b6fa9d66
Modificáronse 73 ficheiros con 4208 adicións e 780 borrados
  1. 39 1
      README.md
  2. 27 2
      alloc/alloc.c
  3. BIN=BIN
      app.png
  4. 2 2
      application.fam
  5. BIN=BIN
      assets/01-home.png
  6. BIN=BIN
      assets/02-town.png
  7. BIN=BIN
      assets/03-town.png
  8. BIN=BIN
      assets/04-town.png
  9. BIN=BIN
      assets/05-tree.png
  10. BIN=BIN
      assets/06-tree.png
  11. 2 0
      assets/CHANGELOG.md
  12. 40 0
      assets/README.md
  13. BIN=BIN
      assets/icon_chest_closed_16x13px.png
  14. BIN=BIN
      assets/icon_chest_open_16x16px.png
  15. BIN=BIN
      assets/icon_fence_16x8px.png
  16. BIN=BIN
      assets/icon_fence_end_16x8px.png
  17. BIN=BIN
      assets/icon_fence_vertical_end_6x8px.png
  18. BIN=BIN
      assets/icon_fence_vertical_start_6x15px.png
  19. BIN=BIN
      assets/icon_house_3d_34x45px.png
  20. BIN=BIN
      assets/icon_house_48x32px.png
  21. BIN=BIN
      assets/icon_lake_bottom_31x12px.png
  22. BIN=BIN
      assets/icon_lake_bottom_left_24x22px.png
  23. BIN=BIN
      assets/icon_lake_bottom_right_24x22px.png
  24. BIN=BIN
      assets/icon_lake_left_11x31px.png
  25. BIN=BIN
      assets/icon_lake_right_11x31.png
  26. BIN=BIN
      assets/icon_lake_top_31x12px.png
  27. BIN=BIN
      assets/icon_lake_top_left_24x22px.png
  28. BIN=BIN
      assets/icon_lake_top_right_24x22px.png
  29. BIN=BIN
      assets/icon_plant_fern_18x16px.png
  30. BIN=BIN
      assets/icon_plant_pointy_13x16px.png
  31. BIN=BIN
      assets/icon_rock_large_18x19px.png
  32. BIN=BIN
      assets/icon_rock_medium_16x14px.png
  33. BIN=BIN
      assets/icon_rock_small_10x8px.png
  34. BIN=BIN
      assets/icon_tree_29x30px.png
  35. BIN=BIN
      assets/icon_tree_48x48px.png
  36. BIN=BIN
      assets/sprites/player.fxbm
  37. BIN=BIN
      assets/sprites/player_left.fxbm
  38. BIN=BIN
      assets/sprites/player_right.fxbm
  39. 931 126
      callback/callback.c
  40. 46 2
      callback/callback.h
  41. 0 48
      draw/draw.c
  42. 0 32
      draw/draw.h
  43. 0 139
      draw/world.c
  44. 0 6
      draw/world.h
  45. 1 0
      easy_flipper/easy_flipper.h
  46. 12 10
      engine/engine.h
  47. 616 14
      flip_storage/storage.c
  48. 31 4
      flip_storage/storage.h
  49. 6 0
      flip_world.c
  50. 36 22
      flip_world.h
  51. 2 1
      flipper_http/flipper_http.c
  52. 2 2
      flipper_http/flipper_http.h
  53. 0 255
      game.c
  54. 0 10
      game.h
  55. 104 0
      game/draw.c
  56. 13 0
      game/draw.h
  57. 214 0
      game/game.c
  58. 28 0
      game/game.h
  59. 509 0
      game/icon.c
  60. 16 0
      game/icon.h
  61. 122 0
      game/level.c
  62. 10 0
      game/level.h
  63. 438 0
      game/world.c
  64. 19 0
      game/world.h
  65. 85 46
      jsmn/jsmn.c
  66. 6 58
      jsmn/jsmn.h
  67. 722 0
      jsmn/jsmn_furi.c
  68. 74 0
      jsmn/jsmn_furi.h
  69. 14 0
      jsmn/jsmn_h.c
  70. 41 0
      jsmn/jsmn_h.h
  71. BIN=BIN
      sprites/player.png
  72. BIN=BIN
      sprites/player_left.png
  73. BIN=BIN
      sprites/player_right.png

+ 39 - 1
README.md

@@ -1,2 +1,40 @@
 # FlipWorld
 # FlipWorld
- Flipper Zero Open World Multiplayer game, best played with the VGM.
+
+The first open-world multiplayer game for the Flipper Zero, best played with the VGM.
+
+## Requirements
+- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
+- 2.4 GHz WiFi access point
+
+## How It Works
+FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which is set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
+
+We've simplified the settings for easy use of the app. In the Game Settings, you can adjust the FPS (30, 60, 120, or 240), download World Packs, and select whether you want the screen backlight to always be on.
+
+The controls in the game are straightforward. Press and hold left to go left, press and hold right to go right, etc. If you press the OK button, it will teleport you to the next available world. **Do NOT spam the OK button** as it could freeze your Flipper Zero. Press it once and wait for it to load.
+
+## Roadmap
+**v0.2**
+- Stability patch
+- Video Game Module support
+
+**v0.3**
+- ???
+
+**v0.4**
+- ???
+
+**v0.5**
+- ???
+
+**v0.6**
+- ???
+
+**v0.7**
+- ???
+
+**v0.8**
+- Multiplayer support
+
+**v1.0**
+- Official release

+ 27 - 2
alloc/alloc.c

@@ -7,7 +7,6 @@
  */
  */
 static uint32_t callback_exit_app(void *context)
 static uint32_t callback_exit_app(void *context)
 {
 {
-    // Exit the application
     UNUSED(context);
     UNUSED(context);
     return VIEW_NONE; // Return VIEW_NONE to exit the app
     return VIEW_NONE; // Return VIEW_NONE to exit the app
 }
 }
@@ -24,6 +23,17 @@ FlipWorldApp *flip_world_app_alloc()
     {
     {
         return NULL;
         return NULL;
     }
     }
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, flip_world_custom_event_callback);
+    // Main view
+    if (!easy_flipper_set_view(&app->view_loader, FlipWorldViewLoader, flip_world_loader_draw_callback, NULL, callback_to_submenu, &app->view_dispatcher, app))
+    {
+        return NULL;
+    }
+    flip_world_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))
@@ -33,6 +43,7 @@ FlipWorldApp *flip_world_app_alloc()
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexRun, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "Play", FlipWorldSubmenuIndexRun, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexAbout, callback_submenu_choices, app);
     submenu_add_item(app->submenu, "About", FlipWorldSubmenuIndexAbout, 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);
+    //
 
 
     // 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);
@@ -55,8 +66,22 @@ 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);
     }
     }
+    // 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);
+        flip_world_loader_free_model(app->view_loader);
+        view_free(app->view_loader);
+    }
 
 
-    free_all_views(app, true);
+    free_all_views(app, true, true);
 
 
     // free the view dispatcher
     // free the view dispatcher
     view_dispatcher_free(app->view_dispatcher);
     view_dispatcher_free(app->view_dispatcher);

BIN=BIN
app.png


+ 2 - 2
application.fam

@@ -6,9 +6,9 @@ App(
     stack_size=4 * 1024,
     stack_size=4 * 1024,
     fap_icon="app.png",
     fap_icon="app.png",
     fap_category="GPIO",
     fap_category="GPIO",
-    fap_description="Open World Multiplayer game, best played with the VGM.",
+    fap_description="The first open-world multiplayer game, best played with the VGM.", 
     fap_icon_assets="assets",
     fap_icon_assets="assets",
-    fap_file_assets="assets", # Do not touch this and the next line, it is needed to generate sprites
+    fap_file_assets="assets", 
     fap_extbuild=(
     fap_extbuild=(
         ExtFile(
         ExtFile(
             path="${FAP_SRC_DIR}/assets",
             path="${FAP_SRC_DIR}/assets",

BIN=BIN
assets/01-home.png


BIN=BIN
assets/02-town.png


BIN=BIN
assets/03-town.png


BIN=BIN
assets/04-town.png


BIN=BIN
assets/05-tree.png


BIN=BIN
assets/06-tree.png


+ 2 - 0
assets/CHANGELOG.md

@@ -0,0 +1,2 @@
+## 0.1
+- Initial Release

+ 40 - 0
assets/README.md

@@ -0,0 +1,40 @@
+# FlipWorld
+
+The first open-world multiplayer game for the Flipper Zero, best played with the VGM.
+
+## Requirements
+- WiFi Developer Board, Raspberry Pi, or ESP32 device with the FlipperHTTP flash: https://github.com/jblanked/FlipperHTTP
+- 2.4 GHz WiFi access point
+
+## How It Works
+FlipWorld and FlipSocial are connected. Your login information is the same in both apps; if you register an account in either app, you can log in to both using that information. This also means that your friends, messages, and achievements are synced between apps. You only need a username and password to start, which is set in the User Settings. Keep in mind your username will be displayed to others, so choose wisely.
+
+We've simplified the settings for easy use of the app. In the Game Settings, you can adjust the FPS (30, 60, 120, or 240), download World Packs, and select whether you want the screen backlight to always be on.
+
+The controls in the game are straightforward. Press and hold left to go left, press and hold right to go right, etc. If you press the OK button, it will teleport you to the next available world. **Do NOT spam the OK button** as it could freeze your Flipper Zero. Press it once and wait for it to load.
+
+## Roadmap
+**v0.2**
+- Stability patch
+- Video Game Module support
+
+**v0.3**
+- ???
+
+**v0.4**
+- ???
+
+**v0.5**
+- ???
+
+**v0.6**
+- ???
+
+**v0.7**
+- ???
+
+**v0.8**
+- Multiplayer support
+
+**v1.0**
+- Official release

BIN=BIN
assets/icon_chest_closed_16x13px.png


BIN=BIN
assets/icon_chest_open_16x16px.png


BIN=BIN
assets/icon_fence_16x8px.png


BIN=BIN
assets/icon_fence_end_16x8px.png


BIN=BIN
assets/icon_fence_vertical_end_6x8px.png


BIN=BIN
assets/icon_fence_vertical_start_6x15px.png


BIN=BIN
assets/icon_house_3d_34x45px.png


BIN=BIN
assets/icon_house_48x32px.png


BIN=BIN
assets/icon_lake_bottom_31x12px.png


BIN=BIN
assets/icon_lake_bottom_left_24x22px.png


BIN=BIN
assets/icon_lake_bottom_right_24x22px.png


BIN=BIN
assets/icon_lake_left_11x31px.png


BIN=BIN
assets/icon_lake_right_11x31.png


BIN=BIN
assets/icon_lake_top_31x12px.png


BIN=BIN
assets/icon_lake_top_left_24x22px.png


BIN=BIN
assets/icon_lake_top_right_24x22px.png


BIN=BIN
assets/icon_plant_fern_18x16px.png


BIN=BIN
assets/icon_plant_pointy_13x16px.png


BIN=BIN
assets/icon_rock_large_18x19px.png


BIN=BIN
assets/icon_rock_medium_16x14px.png


BIN=BIN
assets/icon_rock_small_10x8px.png


BIN=BIN
assets/icon_tree_29x30px.png


BIN=BIN
assets/icon_tree_48x48px.png


BIN=BIN
assets/sprites/player.fxbm


BIN=BIN
assets/sprites/player_left.fxbm


BIN=BIN
assets/sprites/player_right.fxbm


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 931 - 126
callback/callback.c


+ 46 - 2
callback/callback.h

@@ -10,5 +10,49 @@
 #include "engine/level_i.h"
 #include "engine/level_i.h"
 #include "engine/entity_i.h"
 #include "engine/entity_i.h"
 
 
-void free_all_views(void *context, bool free_variable_item_list);
-void callback_submenu_choices(void *context, uint32_t index);
+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);
+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 FlipWorldCustomEvent FlipWorldCustomEvent;
+// enum FlipWorldCustomEvent
+// {
+//     FlipWorldCustomEventProcess,
+// };
+
+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;
+};
+void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoaderFetch fetcher, DataLoaderParser parser, size_t request_count, ViewNavigationCallback back, uint32_t view_id);
+
+void flip_world_loader_draw_callback(Canvas *canvas, void *model);
+
+void flip_world_loader_init(View *view);
+
+void flip_world_loader_free_model(View *view);
+bool flip_world_custom_event_callback(void *context, uint32_t index);

+ 0 - 48
draw/draw.c

@@ -1,48 +0,0 @@
-#include <draw/draw.h>
-
-// Global variables to store camera position
-int camera_x = 0;
-int camera_y = 0;
-
-// Draw a line of icons (16 width)
-void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon)
-{
-    for (int i = 0; i < amount; i++)
-    {
-        if (horizontal)
-        {
-            // check if element is outside the world
-            if (pos.x + (i * 17) > WORLD_WIDTH)
-            {
-                break;
-            }
-
-            canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon);
-        }
-        else
-        {
-            // check if element is outside the world
-            if (pos.y + (i * 17) > WORLD_HEIGHT)
-            {
-                break;
-            }
-
-            canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon);
-        }
-    }
-}
-// Draw a half section of icons (16 width)
-void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon)
-{
-    for (int i = 0; i < 10; i++)
-    {
-        if (right)
-        {
-            draw_icon_line(canvas, (Vector){WORLD_WIDTH / 2 + 6, i * 19 + 2}, 11, true, icon);
-        }
-        else
-        {
-            draw_icon_line(canvas, (Vector){0, i * 19 + 2}, 11, true, icon);
-        }
-    }
-}

+ 0 - 32
draw/draw.h

@@ -1,32 +0,0 @@
-#pragma once
-#include "engine/engine.h"
-#include "flip_world.h"
-#include "flip_world_icons.h"
-
-typedef enum
-{
-    // system draw objects
-    DRAW_DOT,        // canvas_draw_dot
-    DRAW_LINE,       // canvas_draw_line
-    DRAW_BOX,        // canvas_draw_box
-    DRAW_FRAME,      // canvas_draw_frame
-    DRAW_CIRCLE,     // canvas_draw_circle
-    DRAW_XBM,        // canvas_draw_xbm
-                     // custom draw objects
-    DRAW_ICON_EARTH, // 	canvas_draw_icon
-    DRAW_ICON_HOME,  // 	canvas_draw_icon
-    DRAW_ICON_INFO,  // 	canvas_draw_icon
-    DRAW_ICON_MAN,   // 	canvas_draw_man
-    DRAW_ICON_PLANT, // 	canvas_draw_icon
-    DRAW_ICON_TREE,  // 	canvas_draw_icon
-    DRAW_ICON_WOMAN, // 	canvas_draw_icon
-} FlipWorldDrawObjects;
-
-// Global variables to store camera position
-extern int camera_x;
-extern int camera_y;
-
-void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon);
-void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon);
-
-// create custom icons at https://lopaka.app/sandbox

+ 0 - 139
draw/world.c

@@ -1,139 +0,0 @@
-#include <draw/world.h>
-
-void draw_bounds(Canvas *canvas)
-{
-    // Draw the outer bounds adjusted by camera offset
-    // we draw this last to ensure users can see the bounds
-    canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
-}
-
-void draw_example_world(Level *level)
-{
-    spawn_icon(level, &I_icon_earth, 112, 56);
-    spawn_icon(level, &I_icon_home, 128, 24);
-    spawn_icon(level, &I_icon_info, 144, 24);
-    spawn_icon(level, &I_icon_man, 160, 56);
-    spawn_icon(level, &I_icon_woman, 168, 56);
-    spawn_icon(level, &I_icon_plant, 168, 32);
-}
-
-void draw_tree_world(Level *level)
-{
-    // Spawn two full left/up tree lines
-    for (int i = 0; i < 2; i++)
-    {
-        for (int j = 0; j < 22; j++)
-        {
-            spawn_icon(level, &I_icon_tree, 5 + j * 17, 2 + i * 17); // Horizontal lines
-        }
-        for (int j = 0; j < 11; j++)
-        {
-            spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + j * 17); // Vertical lines
-        }
-    }
-
-    // Spawn two full down tree lines
-    for (int i = 9; i < 11; i++)
-    {
-        for (int j = 0; j < 22; j++)
-        {
-            spawn_icon(level, &I_icon_tree, 5 + j * 17, 2 + i * 17); // Horizontal lines
-        }
-    }
-
-    // Spawn two full right tree lines
-    for (int i = 20; i < 22; i++)
-    {
-        for (int j = 0; j < 8; j++)
-        {
-            spawn_icon(level, &I_icon_tree, 5 + i * 17, 50 + j * 17); // Vertical lines
-        }
-    }
-
-    // Spawn labyrinth lines
-    // Third line (14 left, 3 middle, 0 right) - exit line
-    for (int i = 0; i < 14; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 2 * 17);
-    }
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 16 * 17 + i * 17, 2 + 2 * 17);
-    }
-
-    // Fourth line (3 left, 6 middle, 4 right)
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 3 * 17); // 3 left
-    }
-    for (int i = 0; i < 6; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 7 * 17 + i * 17, 2 + 3 * 17); // 6 middle
-    }
-    for (int i = 0; i < 4; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 15 * 17 + i * 17, 2 + 3 * 17); // 4 right
-    }
-
-    // Fifth line (6 left, 7 middle, 0 right)
-    for (int i = 0; i < 6; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 4 * 17); // 6 left
-    }
-    for (int i = 0; i < 7; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 7 * 17 + i * 17, 2 + 4 * 17); // 7 middle
-    }
-
-    // Sixth line (5 left, 6 middle, 7 right)
-    for (int i = 0; i < 5; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 5 * 17); // 5 left
-    }
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 7 * 17 + i * 17, 2 + 5 * 17); // 3 middle
-    }
-    for (int i = 0; i < 7; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 15 * 17 + i * 17, 2 + 5 * 17); // 7 right
-    }
-
-    // Seventh line (0 left, 7 middle, 4 right)
-    for (int i = 0; i < 7; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 6 * 17 + i * 17, 2 + 6 * 17); // 7 middle
-    }
-    for (int i = 0; i < 4; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 14 * 17 + i * 17, 2 + 6 * 17); // 4 right
-    }
-
-    // Eighth line (4 left, 3 middle, 4 right)
-    for (int i = 0; i < 4; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 7 * 17); // 4 left
-    }
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 7 * 17 + i * 17, 2 + 7 * 17); // 3 middle
-    }
-    for (int i = 0; i < 4; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 15 * 17 + i * 17, 2 + 7 * 17); // 4 right
-    }
-
-    // Ninth line (3 left, 2 middle, 3 right)
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + i * 17, 2 + 8 * 17); // 3 left
-    }
-    for (int i = 0; i < 1; i++) // 2 middle
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 5 * 17 + i * 17, 2 + 8 * 17);
-    }
-    for (int i = 0; i < 3; i++)
-    {
-        spawn_icon(level, &I_icon_tree, 5 + 11 * 17 + i * 17, 2 + 8 * 17); // 3 right
-    }
-}

+ 0 - 6
draw/world.h

@@ -1,6 +0,0 @@
-#pragma once
-#include <draw/draw.h>
-#include <game.h>
-void draw_bounds(Canvas *canvas);
-void draw_example_world(Level *level);
-void draw_tree_world(Level *level);

+ 1 - 0
easy_flipper/easy_flipper.h

@@ -24,6 +24,7 @@
 #include <stdio.h>
 #include <stdio.h>
 #include <string.h>
 #include <string.h>
 #include <jsmn/jsmn.h>
 #include <jsmn/jsmn.h>
+#include <jsmn/jsmn_furi.h>
 
 
 #define EASY_TAG "EasyFlipper"
 #define EASY_TAG "EasyFlipper"
 
 

+ 12 - 10
engine/engine.h

@@ -6,19 +6,21 @@
 #include "game_manager.h"
 #include "game_manager.h"
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
-extern "C" {
+extern "C"
+{
 #endif
 #endif
 
 
-typedef struct {
-    float target_fps;
-    bool show_fps;
-    bool always_backlight;
-    void (*start)(GameManager* game_manager, void* context);
-    void (*stop)(void* context);
-    size_t context_size;
-} Game;
+    typedef struct
+    {
+        float target_fps;
+        bool show_fps;
+        bool always_backlight;
+        void (*start)(GameManager *game_manager, void *context);
+        void (*stop)(void *context);
+        size_t context_size;
+    } Game;
 
 
-extern const Game game;
+    extern const Game game;
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 }
 }

+ 616 - 14
flip_storage/storage.c

@@ -1,8 +1,29 @@
 
 
 #include "flip_storage/storage.h"
 #include "flip_storage/storage.h"
 
 
+// Forward declaration for use in other functions
+static bool load_flip_social_settings(
+    char *ssid,
+    size_t ssid_size,
+    char *password,
+    size_t password_size,
+    char *login_username_logged_out,
+    size_t username_out_size,
+    char *login_username_logged_in,
+    size_t username_in_size,
+    char *login_password_logged_out,
+    size_t password_out_size,
+    char *change_password_logged_in,
+    size_t change_password_size,
+    char *change_bio_logged_in,
+    size_t change_bio_size,
+    char *is_logged_in,
+    size_t is_logged_in_size);
+
 void save_settings(
 void save_settings(
-    const char *ssid,
+    const char *wifi_ssid,
+    const char *wifi_password,
+    const char *username,
     const char *password)
     const char *password)
 {
 {
     // Create the directory for saving settings
     // Create the directory for saving settings
@@ -23,12 +44,28 @@ void save_settings(
         return;
         return;
     }
     }
 
 
-    // Save the ssid length and data
-    size_t ssid_length = strlen(ssid) + 1; // Include null terminator
-    if (storage_file_write(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) ||
-        storage_file_write(file, ssid, ssid_length) != ssid_length)
+    // Save the wifi_ssid length and data
+    size_t wifi_ssid_length = strlen(wifi_ssid) + 1; // Include null terminator
+    if (storage_file_write(file, &wifi_ssid_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, wifi_ssid, wifi_ssid_length) != wifi_ssid_length)
     {
     {
-        FURI_LOG_E(TAG, "Failed to write SSID");
+        FURI_LOG_E(TAG, "Failed to write wifi_SSID");
+    }
+
+    // Save the wifi_password length and data
+    size_t wifi_password_length = strlen(wifi_password) + 1; // Include null terminator
+    if (storage_file_write(file, &wifi_password_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, wifi_password, wifi_password_length) != wifi_password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write wifi_password");
+    }
+
+    // Save the username length and data
+    size_t username_length = strlen(username) + 1; // Include null terminator
+    if (storage_file_write(file, &username_length, sizeof(size_t)) != sizeof(size_t) ||
+        storage_file_write(file, username, username_length) != username_length)
+    {
+        FURI_LOG_E(TAG, "Failed to write username");
     }
     }
 
 
     // Save the password length and data
     // Save the password length and data
@@ -45,8 +82,12 @@ void save_settings(
 }
 }
 
 
 bool load_settings(
 bool load_settings(
-    char *ssid,
-    size_t ssid_size,
+    char *wifi_ssid,
+    size_t wifi_ssid_size,
+    char *wifi_password,
+    size_t wifi_password_size,
+    char *username,
+    size_t username_size,
     char *password,
     char *password,
     size_t password_size)
     size_t password_size)
 {
 {
@@ -61,18 +102,44 @@ bool load_settings(
         return false; // Return false if the file does not exist
         return false; // Return false if the file does not exist
     }
     }
 
 
-    // Load the ssid
-    size_t ssid_length;
-    if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size ||
-        storage_file_read(file, ssid, ssid_length) != ssid_length)
+    // Load the wifi_ssid
+    size_t wifi_ssid_length;
+    if (storage_file_read(file, &wifi_ssid_length, sizeof(size_t)) != sizeof(size_t) || wifi_ssid_length > wifi_ssid_size ||
+        storage_file_read(file, wifi_ssid, wifi_ssid_length) != wifi_ssid_length)
     {
     {
-        FURI_LOG_E(TAG, "Failed to read SSID");
+        FURI_LOG_E(TAG, "Failed to read wifi_SSID");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    wifi_ssid[wifi_ssid_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the wifi_password
+    size_t wifi_password_length;
+    if (storage_file_read(file, &wifi_password_length, sizeof(size_t)) != sizeof(size_t) || wifi_password_length > wifi_password_size ||
+        storage_file_read(file, wifi_password, wifi_password_length) != wifi_password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read wifi_password");
         storage_file_close(file);
         storage_file_close(file);
         storage_file_free(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_STORAGE);
         return false;
         return false;
     }
     }
-    ssid[ssid_length - 1] = '\0'; // Ensure null-termination
+    wifi_password[wifi_password_length - 1] = '\0'; // Ensure null-termination
+
+    // Load the username
+    size_t username_length;
+    if (storage_file_read(file, &username_length, sizeof(size_t)) != sizeof(size_t) || username_length > username_size ||
+        storage_file_read(file, username, username_length) != username_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read username");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    username[username_length - 1] = '\0'; // Ensure null-termination
 
 
     // Load the password
     // Load the password
     size_t password_length;
     size_t password_length;
@@ -182,5 +249,540 @@ bool load_char(
     storage_file_free(file);
     storage_file_free(file);
     furi_record_close(RECORD_STORAGE);
     furi_record_close(RECORD_STORAGE);
 
 
+    return true;
+}
+
+bool save_world(
+    const char *name,
+    const char *world_data)
+{
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+
+    // Open the file in write mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = strlen(world_data) + 1; // Include null terminator
+    if (storage_file_write(file, world_data, data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+bool save_world_furi(FuriString *name, FuriString *world_data)
+{
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", furi_string_get_cstr(name));
+
+    // Open the file in write mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = furi_string_size(world_data) + 1; // Include null terminator
+    if (storage_file_write(file, furi_string_get_cstr(world_data), data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+bool load_world(
+    const char *name,
+    char *json_data,
+    size_t json_data_size)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+
+    // Open the file for reading
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL; // Return false if the file does not exist
+    }
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, json_data, json_data_size);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Ensure null-termination
+    json_data[read_count - 1] = '\0';
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+FuriString *load_furi_world(
+    const char *name)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+    char file_path[128];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+    // Open the file for reading
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL; // Return false if the file does not exist
+    }
+
+    // Allocate a FuriString to hold the received data
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+    // Reset the FuriString to ensure it's empty before reading
+    furi_string_reset(str_result);
+
+    // Define a buffer to hold the read data
+    uint8_t *buffer = (uint8_t *)malloc(MAX_FILE_SHOW);
+    if (!buffer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
+        furi_string_free(str_result);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, buffer, MAX_FILE_SHOW);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        furi_string_free(str_result);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Append each byte to the FuriString
+    for (size_t i = 0; i < read_count; i++)
+    {
+        furi_string_push_back(str_result, buffer[i]);
+    }
+
+    // Check if there is more data beyond the maximum size
+    char extra_byte;
+    storage_file_read(file, &extra_byte, 1);
+
+    // Clean up
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    free(buffer);
+    return str_result;
+}
+
+bool world_exists(const char *name)
+{
+    if (!name)
+    {
+        FURI_LOG_E(TAG, "Invalid name");
+        return false;
+    }
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(TAG, "Failed to open storage");
+        return false;
+    }
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+    bool does_exist = storage_file_exists(storage, file_path);
+
+    // Clean up
+    furi_record_close(RECORD_STORAGE);
+    return does_exist;
+}
+
+bool save_world_names(const FuriString *json)
+{
+    // Create the directory for saving settings
+    char directory_path[256];
+    snprintf(directory_path, sizeof(directory_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds");
+
+    // Create the directory
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    storage_common_mkdir(storage, directory_path);
+
+    // Open the settings file
+    File *file = storage_file_alloc(storage);
+    char file_path[128];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
+
+    // Open the file in write mode
+    if (!storage_file_open(file, file_path, FSAM_WRITE, FSOM_CREATE_ALWAYS))
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for writing: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    // Write the data to the file
+    size_t data_size = furi_string_size(json) + 1; // Include null terminator
+    if (storage_file_write(file, furi_string_get_cstr(json), data_size) != data_size)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to append data to file");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
+    return true;
+}
+
+static FuriString *flip_social_info(char *key)
+{
+    char ssid[64];
+    char password[64];
+    char login_username_logged_out[64];
+    char login_username_logged_in[64];
+    char login_password_logged_out[64];
+    char change_password_logged_in[64];
+    char change_bio_logged_in[64];
+    char is_logged_in[64];
+    FuriString *result = furi_string_alloc();
+    if (!result)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate FuriString");
+        return NULL;
+    }
+    if (!load_flip_social_settings(ssid, sizeof(ssid), password, sizeof(password), login_username_logged_out, sizeof(login_username_logged_out), login_username_logged_in, sizeof(login_username_logged_in), login_password_logged_out, sizeof(login_password_logged_out), change_password_logged_in, sizeof(change_password_logged_in), change_bio_logged_in, sizeof(change_bio_logged_in), is_logged_in, sizeof(is_logged_in)))
+    {
+        FURI_LOG_E(TAG, "Failed to load flip social settings");
+        return NULL;
+    }
+    if (strcmp(key, "ssid") == 0)
+    {
+        furi_string_set_str(result, ssid);
+    }
+    else if (strcmp(key, "password") == 0)
+    {
+        furi_string_set_str(result, password);
+    }
+    else if (strcmp(key, "login_username_logged_out") == 0)
+    {
+        furi_string_set_str(result, login_username_logged_out);
+    }
+    else if (strcmp(key, "login_username_logged_in") == 0)
+    {
+        furi_string_set_str(result, login_username_logged_in);
+    }
+    else if (strcmp(key, "login_password_logged_out") == 0)
+    {
+        furi_string_set_str(result, login_password_logged_out);
+    }
+    else if (strcmp(key, "change_password_logged_in") == 0)
+    {
+        furi_string_set_str(result, change_password_logged_in);
+    }
+    else if (strcmp(key, "change_bio_logged_in") == 0)
+    {
+        furi_string_set_str(result, change_bio_logged_in);
+    }
+    else if (strcmp(key, "is_logged_in") == 0)
+    {
+        furi_string_set_str(result, is_logged_in);
+    }
+    else
+    {
+        FURI_LOG_E(TAG, "Invalid key");
+        furi_string_free(result);
+        return NULL;
+    }
+    return result;
+}
+
+bool is_logged_in_to_flip_social()
+{
+    // load flip social settings and check if logged in
+    FuriString *is_logged_in = flip_social_info("is_logged_in");
+    if (!is_logged_in)
+    {
+        FURI_LOG_E(TAG, "Failed to load is_logged_in");
+        return false;
+    }
+    if (furi_string_cmp(is_logged_in, "true") == 0)
+    {
+        // copy the logged_in FlipSocaial settings to FlipWorld
+        FuriString *username = flip_social_info("login_username_logged_in");
+        FuriString *password = flip_social_info("change_password_logged_in");
+        FuriString *wifi_password = flip_social_info("password");
+        FuriString *wifi_ssid = flip_social_info("ssid");
+        if (!username || !password || !wifi_password || !wifi_ssid)
+        {
+            furi_string_free(username);
+            furi_string_free(password);
+            furi_string_free(wifi_password);
+            furi_string_free(wifi_ssid);
+            return false;
+        }
+        save_settings(furi_string_get_cstr(wifi_ssid), furi_string_get_cstr(wifi_password), furi_string_get_cstr(username), furi_string_get_cstr(password));
+        furi_string_free(username);
+        furi_string_free(password);
+        furi_string_free(wifi_password);
+        furi_string_free(wifi_ssid);
+        furi_string_free(is_logged_in);
+        return true;
+    }
+    furi_string_free(is_logged_in);
+    return false;
+}
+
+bool is_logged_in()
+{
+    char is_logged_in[64];
+    if (load_char("is_logged_in", is_logged_in, sizeof(is_logged_in)))
+    {
+        return strcmp(is_logged_in, "true") == 0;
+    }
+    return false;
+}
+
+static bool load_flip_social_settings(
+    char *ssid,
+    size_t ssid_size,
+    char *password,
+    size_t password_size,
+    char *login_username_logged_out,
+    size_t username_out_size,
+    char *login_username_logged_in,
+    size_t username_in_size,
+    char *login_password_logged_out,
+    size_t password_out_size,
+    char *change_password_logged_in,
+    size_t change_password_size,
+    char *change_bio_logged_in,
+    size_t change_bio_size,
+    char *is_logged_in,
+    size_t is_logged_in_size)
+{
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    File *file = storage_file_alloc(storage);
+
+    // file path from flipsocial
+    char file_path[128];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_social/settings.bin");
+    if (!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING))
+    {
+        FURI_LOG_E(TAG, "Failed to open settings file for reading: %s", file_path);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false; // Return false if the file does not exist
+    }
+
+    // Load the ssid
+    size_t ssid_length;
+    if (storage_file_read(file, &ssid_length, sizeof(size_t)) != sizeof(size_t) || ssid_length > ssid_size ||
+        storage_file_read(file, ssid, ssid_length) != ssid_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read SSID");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    else
+    {
+        ssid[ssid_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the password
+    size_t password_length;
+    if (storage_file_read(file, &password_length, sizeof(size_t)) != sizeof(size_t) || password_length > password_size ||
+        storage_file_read(file, password, password_length) != password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read password");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return false;
+    }
+    else
+    {
+        password[password_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the login_username_logged_out
+    size_t username_out_length;
+    if (storage_file_read(file, &username_out_length, sizeof(size_t)) != sizeof(size_t) || username_out_length > username_out_size ||
+        storage_file_read(file, login_username_logged_out, username_out_length) != username_out_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read login_username_logged_out");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        // return false;
+    }
+    else
+    {
+        login_username_logged_out[username_out_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the login_username_logged_in
+    size_t username_in_length;
+    if (storage_file_read(file, &username_in_length, sizeof(size_t)) != sizeof(size_t) || username_in_length > username_in_size ||
+        storage_file_read(file, login_username_logged_in, username_in_length) != username_in_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read login_username_logged_in");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        // return false;
+    }
+    else
+    {
+        login_username_logged_in[username_in_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the login_password_logged_out
+    size_t password_out_length;
+    if (storage_file_read(file, &password_out_length, sizeof(size_t)) != sizeof(size_t) || password_out_length > password_out_size ||
+        storage_file_read(file, login_password_logged_out, password_out_length) != password_out_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read login_password_logged_out");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        // return false;
+    }
+    else
+    {
+        login_password_logged_out[password_out_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the change_password_logged_in
+    size_t change_password_length;
+    if (storage_file_read(file, &change_password_length, sizeof(size_t)) != sizeof(size_t) || change_password_length > change_password_size ||
+        storage_file_read(file, change_password_logged_in, change_password_length) != change_password_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read change_password_logged_in");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        //  return false;
+    }
+    else
+    {
+        change_password_logged_in[change_password_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the is_logged_in
+    size_t is_logged_in_length;
+    if (storage_file_read(file, &is_logged_in_length, sizeof(size_t)) != sizeof(size_t) || is_logged_in_length > is_logged_in_size ||
+        storage_file_read(file, is_logged_in, is_logged_in_length) != is_logged_in_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read is_logged_in");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        //  return false;
+    }
+    else
+    {
+        is_logged_in[is_logged_in_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    // Load the change_bio_logged_in
+    size_t change_bio_length;
+    if (storage_file_read(file, &change_bio_length, sizeof(size_t)) != sizeof(size_t) || change_bio_length > change_bio_size ||
+        storage_file_read(file, change_bio_logged_in, change_bio_length) != change_bio_length)
+    {
+        FURI_LOG_E(TAG, "Failed to read change_bio_logged_in");
+        // storage_file_close(file);
+        // storage_file_free(file);
+        // furi_record_close(RECORD_STORAGE);
+        //  return false;
+    }
+    else
+    {
+        change_bio_logged_in[change_bio_length - 1] = '\0'; // Ensure null-termination
+    }
+
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+
     return true;
     return true;
 }
 }

+ 31 - 4
flip_storage/storage.h

@@ -7,12 +7,18 @@
 #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/settings.bin"
 #define SETTINGS_PATH STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/settings.bin"
 
 
 void save_settings(
 void save_settings(
-    const char *ssid,
+    const char *wifi_ssid,
+    const char *wifi_password,
+    const char *username,
     const char *password);
     const char *password);
 
 
 bool load_settings(
 bool load_settings(
-    char *ssid,
-    size_t ssid_size,
+    char *wifi_ssid,
+    size_t wifi_ssid_size,
+    char *wifi_password,
+    size_t wifi_password_size,
+    char *username,
+    size_t username_size,
     char *password,
     char *password,
     size_t password_size);
     size_t password_size);
 
 
@@ -22,4 +28,25 @@ bool save_char(
 bool load_char(
 bool load_char(
     const char *path_name,
     const char *path_name,
     char *value,
     char *value,
-    size_t value_size);
+    size_t value_size);
+
+bool save_world(
+    const char *name,
+    const char *world_data);
+
+bool save_world_furi(FuriString *name, FuriString *world_data);
+
+bool load_world(
+    const char *name,
+    char *json_data,
+    size_t json_data_size);
+
+FuriString *load_furi_world(
+    const char *name);
+
+bool world_exists(
+    const char *name);
+
+bool save_world_names(const FuriString *json);
+bool is_logged_in_to_flip_social();
+bool is_logged_in();

+ 6 - 0
flip_world.c

@@ -1 +1,7 @@
 #include <flip_world.h>
 #include <flip_world.h>
+char *game_fps_choices[] = {"30", "60", "120", "240"};
+const float game_fps_choices_2[] = {30.0, 60.0, 120.0, 240.0};
+int game_fps_index = 1;
+char *game_screen_always_on_choices[] = {"No", "Yes"};
+int game_screen_always_on_index = 1;
+FlipWorldApp *app_instance = NULL;

+ 36 - 22
flip_world.h

@@ -14,54 +14,68 @@
 #define TAG "FlipWorld"
 #define TAG "FlipWorld"
 #define VERSION_TAG "FlipWorld v0.1"
 #define VERSION_TAG "FlipWorld v0.1"
 
 
-// Screen size
-#define SCREEN_WIDTH 128
-#define SCREEN_HEIGHT 64
-
-// World size (3x3)
-#define WORLD_WIDTH 384
-#define WORLD_HEIGHT 192
-
 // Define the submenu items for our FlipWorld application
 // Define the submenu items for our FlipWorld application
 typedef enum
 typedef enum
 {
 {
     FlipWorldSubmenuIndexRun, // Click to run the FlipWorld application
     FlipWorldSubmenuIndexRun, // Click to run the FlipWorld application
     FlipWorldSubmenuIndexAbout,
     FlipWorldSubmenuIndexAbout,
     FlipWorldSubmenuIndexSettings,
     FlipWorldSubmenuIndexSettings,
+    FlipWorldSubmenuIndexWiFiSettings,
+    FlipWorldSubmenuIndexGameSettings,
+    FlipWorldSubmenuIndexUserSettings,
 } FlipWorldSubmenuIndex;
 } FlipWorldSubmenuIndex;
 
 
 // Define a single view for our FlipWorld application
 // Define a single view for our FlipWorld application
 typedef enum
 typedef enum
 {
 {
-    FlipWorldViewMain,      // The main screen
-    FlipWorldViewSubmenu,   // The submenu
-    FlipWorldViewAbout,     // The about screen
-    FlipWorldViewSettings,  // The settings screen
-    FlipWorldViewTextInput, // The text input screen
+    FlipWorldViewSubmenu,          // The submenu
+    FlipWorldViewAbout,            // The about screen
+    FlipWorldViewSettings,         // The settings screen
+    FlipWorldViewVariableItemList, // The variable item list screen
+    FlipWorldViewTextInput,        // The text input screen
+    //
+    FlipWorldViewWidgetResult, // The text box that displays the random fact
+    FlipWorldViewLoader,       // The loader screen retrieves data from the internet
 } FlipWorldView;
 } FlipWorldView;
 
 
 // Define a custom event for our FlipWorld application
 // Define a custom event for our FlipWorld application
 typedef enum
 typedef enum
 {
 {
+    FlipWorldCustomEventProcess,
     FlipWorldCustomEventPlay, // Play the game
     FlipWorldCustomEventPlay, // Play the game
 } FlipWorldCustomEvent;
 } FlipWorldCustomEvent;
 
 
 // Each screen will have its own view
 // Each screen will have its own view
 typedef struct
 typedef struct
 {
 {
-    // necessary
-    ViewDispatcher *view_dispatcher;      // Switches between our views
-    View *view_main;                      // The game screen
-    View *view_about;                     // The about screen
-    Submenu *submenu;                     // The submenu
-    VariableItemList *variable_item_list; // The variable item list (settngs)
-    VariableItem *variable_item_ssid;     // The variable item
-    VariableItem *variable_item_pass;     // The variable item
+    View *view_loader;
+    Widget *widget_result;
+    //
+    ViewDispatcher *view_dispatcher;       // Switches between our views
+    View *view_about;                      // The about screen
+    Submenu *submenu;                      // The submenu
+    Submenu *submenu_settings;             // The settings submenu
+    VariableItemList *variable_item_list;  // The variable item list (settngs)
+    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_game_fps;              // The variable item for Game FPS
+    VariableItem *variable_item_game_screen_always_on; // The variable item for Screen always on
+    VariableItem *variable_item_game_download_world;   // The variable item for Download world
+    //
+    VariableItem *variable_item_user_username; // The variable item for the User username
+    VariableItem *variable_item_user_password; // The variable item for the User password
 
 
     UART_TextInput *text_input;      // The text input
     UART_TextInput *text_input;      // The text input
     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;
 
 
-// TODO - Add Download world function and download world pack button
+extern char *game_fps_choices[];
+extern const float game_fps_choices_2[];
+extern int game_fps_index;
+extern char *game_screen_always_on_choices[];
+extern int game_screen_always_on_index;
+extern FlipWorldApp *app_instance;

+ 2 - 1
flipper_http/flipper_http.c

@@ -89,7 +89,8 @@ FuriString *flipper_http_load_from_file(char *file_path)
     {
     {
         storage_file_free(file);
         storage_file_free(file);
         furi_record_close(RECORD_STORAGE);
         furi_record_close(RECORD_STORAGE);
-        return NULL; // Return false if the file does not exist
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path);
+        return NULL;
     }
     }
 
 
     // Allocate a FuriString to hold the received data
     // Allocate a FuriString to hold the received data

+ 2 - 2
flipper_http/flipper_http.h

@@ -22,9 +22,9 @@
 #define UART_CH (FuriHalSerialIdUsart)    // UART channel
 #define UART_CH (FuriHalSerialIdUsart)    // UART channel
 #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 1024                  // UART RX buffer size
+#define RX_BUF_SIZE 2048                  // UART RX buffer size
 #define RX_LINE_BUFFER_SIZE 4096          // UART RX line buffer size (increase for large responses)
 #define RX_LINE_BUFFER_SIZE 4096          // UART RX line buffer size (increase for large responses)
-#define MAX_FILE_SHOW 4096                // 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
 
 
 // Forward declaration for callback
 // Forward declaration for callback

+ 0 - 255
game.c

@@ -1,255 +0,0 @@
-#include "game.h"
-#include "flip_world.h"
-#include "flip_world_icons.h"
-
-// Background rendering function
-// TODO: each object needs a collision box so we can detect collisions and prevent movement through walls.
-static void background_render(Canvas *canvas, Vector pos)
-{
-    // Clear the canvas
-    canvas_clear(canvas);
-
-    // Calculate camera offset to center the player
-    camera_x = pos.x - (SCREEN_WIDTH / 2);
-    camera_y = pos.y - (SCREEN_HEIGHT / 2);
-
-    // Clamp camera position to prevent showing areas outside the world
-    camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
-    camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
-
-    // Draw the outer bounds adjusted by camera offset
-    draw_bounds(canvas);
-}
-
-/****** Entities: Player ******/
-
-typedef struct
-{
-    Vector trajectory; // Direction player would like to move.
-    float radius;      // collision radius
-    int8_t dx;         // x direction
-    int8_t dy;         // y direction
-    Sprite *sprite;    // player sprite
-} PlayerContext;
-
-// Forward declaration of player_desc, because it's used in player_spawn function.
-static const EntityDescription player_desc;
-
-static void player_spawn(Level *level, GameManager *manager)
-{
-    Entity *player = level_add_entity(level, &player_desc);
-
-    // Set player position.
-    // Depends on your game logic, it can be done in start entity function, but also can be done here.
-    entity_pos_set(player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
-
-    // Add collision box to player entity
-    // Box is centered in player x and y, and it's size is 10x10
-    entity_collider_add_rect(player, 10, 10);
-
-    // Get player context
-    PlayerContext *player_context = entity_context_get(player);
-
-    // Load player sprite
-    player_context->sprite = game_manager_sprite_load(manager, "player.fxbm");
-}
-
-// Modify player_update to track direction
-static void player_update(Entity *self, GameManager *manager, void *context)
-{
-    PlayerContext *player = (PlayerContext *)context;
-    InputState input = game_manager_input_get(manager);
-    Vector pos = entity_pos_get(self);
-
-    // Reset direction each frame
-    player->dx = 0;
-    player->dy = 0;
-
-    if (input.held & GameKeyUp)
-    {
-        pos.y -= 2;
-        player->dy = -1;
-    }
-    if (input.held & GameKeyDown)
-    {
-        pos.y += 2;
-        player->dy = 1;
-    }
-    if (input.held & GameKeyLeft)
-    {
-        pos.x -= 2;
-        player->dx = -1;
-    }
-    if (input.held & GameKeyRight)
-    {
-        pos.x += 2;
-        player->dx = 1;
-    }
-
-    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
-    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
-
-    entity_pos_set(self, pos);
-
-    if (input.pressed & GameKeyBack)
-    {
-        game_manager_game_stop(manager);
-    }
-}
-
-static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
-{
-    // Get player context
-    UNUSED(manager);
-    PlayerContext *player = context;
-
-    // Get player position
-    Vector pos = entity_pos_get(self);
-
-    // Draw background (updates camera_x and camera_y)
-    background_render(canvas, pos);
-
-    // Draw player sprite relative to camera
-    canvas_draw_sprite(canvas, player->sprite, pos.x - camera_x - 5, pos.y - camera_y - 5);
-}
-
-static const EntityDescription player_desc = {
-    .start = NULL,                         // called when entity is added to the level
-    .stop = NULL,                          // called when entity is removed from the level
-    .update = player_update,               // called every frame
-    .render = player_render,               // called every frame, after update
-    .collision = NULL,                     // called when entity collides with another entity
-    .event = NULL,                         // called when entity receives an event
-    .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
-};
-
-/****** Level ******/
-
-static void level_alloc(Level *level, GameManager *manager, void *context)
-{
-    UNUSED(manager);
-    UNUSED(context);
-
-    // Add player entity to the level
-    player_spawn(level, manager);
-
-    draw_tree_world(level);
-    // draw_example_world(level);
-}
-
-static const LevelBehaviour level = {
-    .alloc = level_alloc, // called once, when level allocated
-    .free = NULL,         // called once, when level freed
-    .start = NULL,        // called when level is changed to this level
-    .stop = NULL,         // called when level is changed from this level
-    .context_size = 0,    // size of level context, will be automatically allocated and freed
-};
-
-typedef struct
-{
-    const Icon *icon;
-} IconContext;
-
-// Forward declaration of icon_desc
-static const EntityDescription icon_desc;
-
-static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context)
-{
-    UNUSED(manager);
-    UNUSED(self);
-    IconContext *icon = (IconContext *)context;
-    UNUSED(icon);
-    if (entity_description_get(other) == &player_desc)
-    {
-        PlayerContext *player = (PlayerContext *)entity_context_get(other);
-        if (player)
-        {
-            Vector pos = entity_pos_get(other);
-            // Bounce the player back by 3 units opposite their last movement direction
-            pos.x -= player->dx * 3;
-            pos.y -= player->dy * 3;
-            entity_pos_set(other, pos);
-        }
-    }
-}
-
-static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
-{
-    UNUSED(manager);
-    IconContext *icon_ctx = (IconContext *)context;
-    Vector pos = entity_pos_get(self);
-    canvas_draw_icon(canvas, pos.x - camera_x - 8, pos.y - camera_y - 8, icon_ctx->icon);
-}
-
-static void icon_start(Entity *self, GameManager *manager, void *context)
-{
-    UNUSED(manager);
-    UNUSED(context);
-    // Just add the collision rectangle for 16x16 icon
-    entity_collider_add_rect(self, 16, 16);
-}
-
-static const EntityDescription icon_desc = {
-    .start = icon_start,
-    .stop = NULL,
-    .update = NULL,
-    .render = icon_render,
-    .collision = icon_collision,
-    .event = NULL,
-    .context_size = sizeof(IconContext),
-};
-
-// Helper function to spawn an icon entity at a given position
-void spawn_icon(Level *level, const Icon *icon, float x, float y)
-{
-    Entity *e = level_add_entity(level, &icon_desc);
-    IconContext *icon_ctx = entity_context_get(e);
-    icon_ctx->icon = icon;
-    // Set the entity position to the center of the icon
-    entity_pos_set(e, (Vector){x + 8, y + 8});
-}
-
-/****** Game ******/
-
-/*
-    Write here the start code for your game, for example: creating a level and so on.
-    Game context is allocated (game.context_size) and passed to this function, you can use it to store your game data.
-*/
-static void game_start(GameManager *game_manager, void *ctx)
-{
-    UNUSED(game_manager);
-
-    // Do some initialization here, for example you can load score from storage.
-    // For simplicity, we will just set it to 0.
-    GameContext *game_context = ctx;
-    game_context->score = 0;
-
-    // Add level to the game
-    game_manager_add_level(game_manager, &level);
-}
-
-/*
-    Write here the stop code for your game, for example, freeing memory, if it was allocated.
-    You don't need to free level, sprites or entities, it will be done automatically.
-    Also, you don't need to free game_context, it will be done automatically, after this function.
-*/
-static void game_stop(void *ctx)
-{
-    UNUSED(ctx);
-    // GameContext *game_context = ctx;
-    //  Do some deinitialization here, for example you can save score to storage.
-    //  For simplicity, we will just print it.
-    // FURI_LOG_I("Game", "Your score: %lu", game_context->score);
-}
-
-/*
-    Your game configuration, do not rename this variable, but you can change its content here.
-*/
-const Game game = {
-    .target_fps = 30,                    // target fps, game will try to keep this value
-    .show_fps = false,                   // show fps counter on the screen
-    .always_backlight = true,            // keep display backlight always on
-    .start = game_start,                 // will be called once, when game starts
-    .stop = game_stop,                   // will be called once, when game stops
-    .context_size = sizeof(GameContext), // size of game context
-};

+ 0 - 10
game.h

@@ -1,10 +0,0 @@
-#pragma once
-#include "engine/engine.h"
-#include <draw/world.h>
-
-void spawn_icon(Level *level, const Icon *icon, float x, float y);
-
-typedef struct
-{
-    uint32_t score;
-} GameContext;

+ 104 - 0
game/draw.c

@@ -0,0 +1,104 @@
+#include <game/draw.h>
+
+// Global variables to store camera position
+int camera_x = 0;
+int camera_y = 0;
+
+// Background rendering function (no collision detection)
+void draw_background(Canvas *canvas, Vector pos)
+{
+    // Clear the canvas
+    canvas_clear(canvas);
+
+    // Calculate camera offset to center the player
+    camera_x = pos.x - (SCREEN_WIDTH / 2);
+    camera_y = pos.y - (SCREEN_HEIGHT / 2);
+
+    // Clamp camera position to prevent showing areas outside the world
+    camera_x = CLAMP(camera_x, WORLD_WIDTH - SCREEN_WIDTH, 0);
+    camera_y = CLAMP(camera_y, WORLD_HEIGHT - SCREEN_HEIGHT, 0);
+
+    // Draw the outer bounds adjusted by camera offset
+    draw_bounds(canvas);
+}
+
+// Draw a line of icons (16 width)
+void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon)
+{
+    for (int i = 0; i < amount; i++)
+    {
+        if (horizontal)
+        {
+            // check if element is outside the world
+            if (pos.x + (i * 17) > WORLD_WIDTH)
+            {
+                break;
+            }
+
+            canvas_draw_icon(canvas, pos.x + (i * 17) - camera_x, pos.y - camera_y, icon);
+        }
+        else
+        {
+            // check if element is outside the world
+            if (pos.y + (i * 17) > WORLD_HEIGHT)
+            {
+                break;
+            }
+
+            canvas_draw_icon(canvas, pos.x - camera_x, pos.y + (i * 17) - camera_y, icon);
+        }
+    }
+}
+// Draw a half section of icons (16 width)
+void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon)
+{
+    for (int i = 0; i < 10; i++)
+    {
+        if (right)
+        {
+            draw_icon_line(canvas, (Vector){WORLD_WIDTH / 2 + 6, i * 19 + 2}, 11, true, icon);
+        }
+        else
+        {
+            draw_icon_line(canvas, (Vector){0, i * 19 + 2}, 11, true, icon);
+        }
+    }
+}
+// Draw an icon at a specific position (with collision detection)
+void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height)
+{
+    Entity *e = level_add_entity(level, &icon_desc);
+    IconContext *icon_ctx = entity_context_get(e);
+    icon_ctx->icon = icon;
+    icon_ctx->width = width;
+    icon_ctx->height = height;
+    // Set the entity position to the center of the icon
+    entity_pos_set(e, (Vector){x + (width / 2), y + (height / 2)});
+}
+// Draw a line of icons at a specific position (with collision detection)
+void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal)
+{
+    for (int i = 0; i < amount; i++)
+    {
+        if (horizontal)
+        {
+            // check if element is outside the world
+            if (x + (i * 17) > WORLD_WIDTH)
+            {
+                break;
+            }
+
+            spawn_icon(level, icon, x + (i * 17), y, width, height);
+        }
+        else
+        {
+            // check if element is outside the world
+            if (y + (i * 17) > WORLD_HEIGHT)
+            {
+                break;
+            }
+
+            spawn_icon(level, icon, x, y + (i * 17), width, height);
+        }
+    }
+}

+ 13 - 0
game/draw.h

@@ -0,0 +1,13 @@
+#pragma once
+#include "game/icon.h"
+
+// Global variables to store camera position
+extern int camera_x;
+extern int camera_y;
+void draw_background(Canvas *canvas, Vector pos);
+void draw_icon_line(Canvas *canvas, Vector pos, int amount, bool horizontal, const Icon *icon);
+void draw_icon_half_world(Canvas *canvas, bool right, const Icon *icon);
+void spawn_icon(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height);
+void spawn_icon_line(Level *level, const Icon *icon, float x, float y, uint8_t width, uint8_t height, uint8_t amount, bool horizontal);
+
+// create custom icons at https://lopaka.app/sandbox

+ 214 - 0
game/game.c

@@ -0,0 +1,214 @@
+#include "game.h"
+
+/****** Entities: Player ******/
+static Level *levels[10];
+static int level_count = 0;
+static bool has_pressed_ok = false;
+
+static Level *get_next_level(GameManager *manager)
+{
+    has_pressed_ok = false;
+    Level *current_level = game_manager_current_level_get(manager);
+    for (int i = 0; i < level_count; i++)
+    {
+        if (levels[i] == current_level)
+        {
+            // check if i+1 is out of bounds, if so, return the first level
+            return levels[(i + 1) % level_count] ? levels[(i + 1) % level_count] : levels[0];
+        }
+    }
+    return levels[0] ? levels[0] : game_manager_add_level(manager, generic_level("town_world", 0));
+}
+
+void player_spawn(Level *level, GameManager *manager)
+{
+    Entity *player = level_add_entity(level, &player_desc);
+
+    // Set player position.
+    // Depends on your game logic, it can be done in start entity function, but also can be done here.
+    entity_pos_set(player, (Vector){WORLD_WIDTH / 2, WORLD_HEIGHT / 2});
+
+    // Add collision box to player entity
+    // Box is centered in player x and y, and it's size is 10x10
+    entity_collider_add_rect(player, 10 + PLAYER_COLLISION_HORIZONTAL, 10 + PLAYER_COLLISION_VERTICAL);
+
+    // Get player context
+    PlayerContext *player_context = entity_context_get(player);
+
+    // Load player sprite
+    player_context->sprite_right = game_manager_sprite_load(manager, "player_right.fxbm");
+    player_context->sprite_left = game_manager_sprite_load(manager, "player_left.fxbm");
+    player_context->is_looking_left = false; // player starts looking right
+}
+
+// Modify player_update to track direction
+static void player_update(Entity *self, GameManager *manager, void *context)
+{
+    PlayerContext *player = (PlayerContext *)context;
+    InputState input = game_manager_input_get(manager);
+    Vector pos = entity_pos_get(self);
+
+    // Store previous direction
+    int prev_dx = player->dx;
+    int prev_dy = player->dy;
+
+    // Reset movement deltas each frame
+    player->dx = 0;
+    player->dy = 0;
+
+    // Handle movement input
+    if (input.held & GameKeyUp)
+    {
+        pos.y -= 2;
+        player->dy = -1;
+    }
+    if (input.held & GameKeyDown)
+    {
+        pos.y += 2;
+        player->dy = 1;
+    }
+    if (input.held & GameKeyLeft)
+    {
+        pos.x -= 2;
+        player->dx = -1;
+        player->is_looking_left = true;
+    }
+    if (input.held & GameKeyRight)
+    {
+        pos.x += 2;
+        player->dx = 1;
+        player->is_looking_left = false;
+    }
+
+    // switch levels if holding OK
+    if (input.held & GameKeyOk)
+    {
+        if (!has_pressed_ok)
+        {
+            has_pressed_ok = true;
+            game_manager_next_level_set(manager, get_next_level(manager));
+            furi_delay_ms(500);
+        }
+        return;
+    }
+
+    // Clamp the player's position to stay within world bounds
+    pos.x = CLAMP(pos.x, WORLD_WIDTH - 5, 5);
+    pos.y = CLAMP(pos.y, WORLD_HEIGHT - 5, 5);
+
+    // Update player position
+    entity_pos_set(self, pos);
+
+    // If the player is not moving, retain the last movement direction
+    if (player->dx == 0 && player->dy == 0)
+    {
+        player->dx = prev_dx;
+        player->dy = prev_dy;
+    }
+
+    // Handle back button to stop the game
+    if (input.pressed & GameKeyBack)
+    {
+        game_manager_game_stop(manager);
+    }
+}
+
+static void player_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    // Get player context
+    UNUSED(manager);
+    PlayerContext *player = context;
+
+    // Get player position
+    Vector pos = entity_pos_get(self);
+
+    // Draw background (updates camera_x and camera_y)
+    draw_background(canvas, pos);
+
+    // Draw player sprite relative to camera, centered on the player's position
+    canvas_draw_sprite(
+        canvas,
+        player->is_looking_left ? player->sprite_left : player->sprite_right,
+        pos.x - camera_x - 5, // Center the sprite horizontally
+        pos.y - camera_y - 5  // Center the sprite vertically
+    );
+}
+
+const EntityDescription player_desc = {
+    .start = NULL,                         // called when entity is added to the level
+    .stop = NULL,                          // called when entity is removed from the level
+    .update = player_update,               // called every frame
+    .render = player_render,               // called every frame, after update
+    .collision = NULL,                     // called when entity collides with another entity
+    .event = NULL,                         // called when entity receives an event
+    .context_size = sizeof(PlayerContext), // size of entity context, will be automatically allocated and freed
+};
+
+/****** Game ******/
+/*
+    Write here the start code for your game, for example: creating a level and so on.
+    Game context is allocated (game.context_size) and passed to this function, you can use it to store your game data.
+*/
+static void game_start(GameManager *game_manager, void *ctx)
+{
+    // Do some initialization here, for example you can load score from storage.
+    // For simplicity, we will just set it to 0.
+    GameContext *game_context = ctx;
+    game_context->score = 0;
+
+    // open the world list from storage, then create a level for each world
+    char file_path[128];
+    snprintf(file_path, sizeof(file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
+    FuriString *world_list = flipper_http_load_from_file(file_path);
+    if (!world_list)
+    {
+        FURI_LOG_E("Game", "Failed to load world list");
+        levels[0] = game_manager_add_level(game_manager, generic_level("town_world", 0));
+        level_count = 1;
+        return;
+    }
+    for (int i = 0; i < 10; i++)
+    {
+        FuriString *world_name = get_json_array_value_furi("worlds", i, world_list);
+        if (!world_name)
+        {
+            break;
+        }
+        levels[i] = game_manager_add_level(game_manager, generic_level(furi_string_get_cstr(world_name), i));
+        furi_string_free(world_name);
+        level_count++;
+    }
+    furi_string_free(world_list);
+}
+
+/*
+    Write here the stop code for your game, for example, freeing memory, if it was allocated.
+    You don't need to free level, sprites or entities, it will be done automatically.
+    Also, you don't need to free game_context, it will be done automatically, after this function.
+*/
+static void game_stop(void *ctx)
+{
+    UNUSED(ctx);
+    // If you want to do other final logic (like saving scores), do it here.
+    // But do NOT free levels[] if the engine manages them.
+
+    // Just clear out your pointer array if you like (not strictly necessary)
+    for (int i = 0; i < level_count; i++)
+    {
+        levels[i] = NULL;
+    }
+    level_count = 0;
+}
+
+/*
+    Your game configuration, do not rename this variable, but you can change its content here.
+*/
+
+const Game game = {
+    .target_fps = 0,                     // set to 0 because we set this in game_app (callback.c line 22)
+    .show_fps = false,                   // show fps counter on the screen
+    .always_backlight = true,            // keep display backlight always on
+    .start = game_start,                 // will be called once, when game starts
+    .stop = game_stop,                   // will be called once, when game stops
+    .context_size = sizeof(GameContext), // size of game context
+};

+ 28 - 0
game/game.h

@@ -0,0 +1,28 @@
+#pragma once
+#include "engine/engine.h"
+#include <game/world.h>
+#include <game/level.h>
+#include "flip_world.h"
+#include "flip_storage/storage.h"
+
+#define PLAYER_COLLISION_VERTICAL 5
+#define PLAYER_COLLISION_HORIZONTAL 5
+
+typedef struct
+{
+    uint32_t score;
+} GameContext;
+
+typedef struct
+{
+    Vector trajectory;    // Direction player would like to move.
+    float radius;         // collision radius
+    int8_t dx;            // x direction
+    int8_t dy;            // y direction
+    Sprite *sprite_right; // player sprite
+    Sprite *sprite_left;  // player sprite looking left
+    bool is_looking_left; // player is looking left
+} PlayerContext;
+
+extern const EntityDescription player_desc;
+void player_spawn(Level *level, GameManager *manager);

+ 509 - 0
game/icon.c

@@ -0,0 +1,509 @@
+#include "game/icon.h"
+
+// Icon entity description
+
+static void icon_collision(Entity *self, Entity *other, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    UNUSED(self);
+    IconContext *icon = (IconContext *)context;
+    UNUSED(icon);
+
+    if (entity_description_get(other) == &player_desc)
+    {
+        PlayerContext *player = (PlayerContext *)entity_context_get(other);
+        if (player)
+        {
+            Vector pos = entity_pos_get(other);
+
+            // Bounce the player back by 2 units opposite their last movement direction
+            pos.x -= player->dx * 2;
+            pos.y -= player->dy * 2;
+            entity_pos_set(other, pos);
+
+            // Reset player's movement direction to prevent immediate re-collision
+            player->dx = 0;
+            player->dy = 0;
+        }
+    }
+}
+
+static void icon_render(Entity *self, GameManager *manager, Canvas *canvas, void *context)
+{
+    UNUSED(manager);
+    IconContext *icon_ctx = (IconContext *)context;
+    Vector pos = entity_pos_get(self);
+    canvas_draw_icon(canvas, pos.x - camera_x - icon_ctx->width / 2, pos.y - camera_y - icon_ctx->height / 2, icon_ctx->icon);
+}
+
+static void icon_start(Entity *self, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    IconContext *icon_ctx = (IconContext *)context;
+    // Just add the collision rectangle for 16x16 icon
+    entity_collider_add_rect(self, icon_ctx->width + COLLISION_BOX_PADDING_HORIZONTAL, icon_ctx->height + COLLISION_BOX_PADDING_VERTICAL);
+}
+
+const EntityDescription icon_desc = {
+    .start = icon_start,
+    .stop = NULL,
+    .update = NULL,
+    .render = icon_render,
+    .collision = icon_collision,
+    .event = NULL,
+    .context_size = sizeof(IconContext),
+};
+
+static IconContext _generic_icon = {
+    .icon = &I_icon_earth_15x16,
+    .width = 15,
+    .height = 16,
+};
+
+IconContext *get_icon_context(char *name)
+{
+    if (strcmp(name, "earth") == 0)
+    {
+        _generic_icon.icon = &I_icon_earth_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "home") == 0)
+    {
+        _generic_icon.icon = &I_icon_home_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "house") == 0)
+    {
+        _generic_icon.icon = &I_icon_house_48x32px;
+        _generic_icon.width = 48;
+        _generic_icon.height = 32;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "house_3d") == 0)
+    {
+        _generic_icon.icon = &I_icon_house_3d_34x45px;
+        _generic_icon.width = 34;
+        _generic_icon.height = 45;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "info") == 0)
+    {
+        _generic_icon.icon = &I_icon_info_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "man") == 0)
+    {
+        _generic_icon.icon = &I_icon_man_7x16;
+        _generic_icon.width = 7;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "plant") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "plant_fern") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_fern_18x16px;
+        _generic_icon.width = 18;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "plant_pointy") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_pointy_13x16px;
+        _generic_icon.width = 13;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "tree") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "tree_29x30") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_29x30px;
+        _generic_icon.width = 29;
+        _generic_icon.height = 30;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "tree_48x48") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_48x48px;
+        _generic_icon.width = 48;
+        _generic_icon.height = 48;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "woman") == 0)
+    {
+        _generic_icon.icon = &I_icon_woman_9x16;
+        _generic_icon.width = 9;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "chest_closed") == 0)
+    {
+        _generic_icon.icon = &I_icon_chest_closed_16x13px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 13;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "chest_open") == 0)
+    {
+        _generic_icon.icon = &I_icon_chest_open_16x16px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "fence") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_16x8px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "fence_end") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_end_16x8px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "fence_vertical_end") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_vertical_end_6x8px;
+        _generic_icon.width = 6;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "fence_vertical_start") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_vertical_start_6x15px;
+        _generic_icon.width = 6;
+        _generic_icon.height = 15;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "flower") == 0)
+    {
+        _generic_icon.icon = &I_icon_flower_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_bottom") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_31x12px;
+        _generic_icon.width = 31;
+        _generic_icon.height = 12;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_bottom_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_left_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_bottom_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_right_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_left_11x31px;
+        _generic_icon.width = 11;
+        _generic_icon.height = 31;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_right_11x31; // Assuming it's 11x31
+        _generic_icon.width = 11;
+        _generic_icon.height = 31;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_top") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_31x12px;
+        _generic_icon.width = 31;
+        _generic_icon.height = 12;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_top_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_left_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "lake_top_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_right_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "rock_large") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_large_18x19px;
+        _generic_icon.width = 18;
+        _generic_icon.height = 19;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "rock_medium") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_medium_16x14px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 14;
+        return &_generic_icon;
+    }
+    if (strcmp(name, "rock_small") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_small_10x8px;
+        _generic_icon.width = 10;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+
+    // If no match is found
+    return NULL;
+}
+
+IconContext *get_icon_context_furi(FuriString *name)
+{
+    if (furi_string_cmp(name, "earth") == 0)
+    {
+        _generic_icon.icon = &I_icon_earth_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "home") == 0)
+    {
+        _generic_icon.icon = &I_icon_home_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "house") == 0)
+    {
+        _generic_icon.icon = &I_icon_house_48x32px;
+        _generic_icon.width = 48;
+        _generic_icon.height = 32;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "house_3d") == 0)
+    {
+        _generic_icon.icon = &I_icon_house_3d_34x45px;
+        _generic_icon.width = 34;
+        _generic_icon.height = 45;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "info") == 0)
+    {
+        _generic_icon.icon = &I_icon_info_15x16;
+        _generic_icon.width = 15;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "man") == 0)
+    {
+        _generic_icon.icon = &I_icon_man_7x16;
+        _generic_icon.width = 7;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "plant") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "plant_fern") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_fern_18x16px;
+        _generic_icon.width = 18;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "plant_pointy") == 0)
+    {
+        _generic_icon.icon = &I_icon_plant_pointy_13x16px;
+        _generic_icon.width = 13;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "tree") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "tree_29x30") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_29x30px;
+        _generic_icon.width = 29;
+        _generic_icon.height = 30;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "tree_48x48") == 0)
+    {
+        _generic_icon.icon = &I_icon_tree_48x48px;
+        _generic_icon.width = 48;
+        _generic_icon.height = 48;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "woman") == 0)
+    {
+        _generic_icon.icon = &I_icon_woman_9x16;
+        _generic_icon.width = 9;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "chest_closed") == 0)
+    {
+        _generic_icon.icon = &I_icon_chest_closed_16x13px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 13;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "chest_open") == 0)
+    {
+        _generic_icon.icon = &I_icon_chest_open_16x16px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "fence") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_16x8px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "fence_end") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_end_16x8px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "fence_vertical_end") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_vertical_end_6x8px;
+        _generic_icon.width = 6;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "fence_vertical_start") == 0)
+    {
+        _generic_icon.icon = &I_icon_fence_vertical_start_6x15px;
+        _generic_icon.width = 6;
+        _generic_icon.height = 15;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "flower") == 0)
+    {
+        _generic_icon.icon = &I_icon_flower_16x16;
+        _generic_icon.width = 16;
+        _generic_icon.height = 16;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_bottom") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_31x12px;
+        _generic_icon.width = 31;
+        _generic_icon.height = 12;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_bottom_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_left_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_bottom_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_bottom_right_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_left_11x31px;
+        _generic_icon.width = 11;
+        _generic_icon.height = 31;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_right_11x31; // Assuming dimensions
+        _generic_icon.width = 11;
+        _generic_icon.height = 31;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_top") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_31x12px;
+        _generic_icon.width = 31;
+        _generic_icon.height = 12;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_top_left") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_left_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "lake_top_right") == 0)
+    {
+        _generic_icon.icon = &I_icon_lake_top_right_24x22px;
+        _generic_icon.width = 24;
+        _generic_icon.height = 22;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "rock_large") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_large_18x19px;
+        _generic_icon.width = 18;
+        _generic_icon.height = 19;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "rock_medium") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_medium_16x14px;
+        _generic_icon.width = 16;
+        _generic_icon.height = 14;
+        return &_generic_icon;
+    }
+    if (furi_string_cmp(name, "rock_small") == 0)
+    {
+        _generic_icon.icon = &I_icon_rock_small_10x8px;
+        _generic_icon.width = 10;
+        _generic_icon.height = 8;
+        return &_generic_icon;
+    }
+
+    // If no match is found
+    return NULL;
+}

+ 16 - 0
game/icon.h

@@ -0,0 +1,16 @@
+#pragma once
+#include "flip_world_icons.h"
+#include "game.h"
+#define COLLISION_BOX_PADDING_HORIZONTAL 10
+#define COLLISION_BOX_PADDING_VERTICAL 12
+
+typedef struct
+{
+    const Icon *icon;
+    uint8_t width;
+    uint8_t height;
+} IconContext;
+
+extern const EntityDescription icon_desc;
+IconContext *get_icon_context(char *name);
+IconContext *get_icon_context_furi(FuriString *name);

+ 122 - 0
game/level.c

@@ -0,0 +1,122 @@
+#include <game/level.h>
+
+static void level_start(Level *level, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    if (!level || !context)
+    {
+        FURI_LOG_E("Game", "Level or context is NULL");
+        return;
+    }
+
+    level_clear(level);
+    player_spawn(level, manager);
+    LevelContext *level_context = context;
+
+    // check if the world exists
+    if (!world_exists(level_context->id))
+    {
+        FURI_LOG_E("Game", "World does not exist.. downloading now");
+        FuriString *world_data = fetch_world(level_context->id);
+        if (!world_data)
+        {
+            FURI_LOG_E("Game", "Failed to fetch world data");
+            draw_tree_world(level);
+            return;
+        }
+
+        if (!draw_json_world(level, furi_string_get_cstr(world_data)))
+        {
+            FURI_LOG_E("Game", "Failed to draw world");
+            draw_tree_world(level);
+        }
+
+        // world_data is guaranteed non-NULL here
+        furi_string_free(world_data);
+        return;
+    }
+
+    // get the world data
+    FuriString *world_data = load_furi_world(level_context->id);
+    if (!world_data)
+    {
+        FURI_LOG_E("Game", "Failed to load world data");
+        draw_tree_world(level);
+        return;
+    }
+
+    // draw the world
+    if (!draw_json_world(level, furi_string_get_cstr(world_data)))
+    {
+        FURI_LOG_E("Game", "World exists but failed to draw.");
+        draw_tree_world(level);
+    }
+
+    // world_data is guaranteed non-NULL here
+    furi_string_free(world_data);
+}
+
+static LevelContext *level_context_generic;
+
+static LevelContext *level_generic_alloc(const char *id, int index)
+{
+    if (level_context_generic == NULL)
+    {
+        level_context_generic = malloc(sizeof(LevelContext));
+    }
+    snprintf(level_context_generic->id, sizeof(level_context_generic->id), "%s", id);
+    level_context_generic->index = index;
+    return level_context_generic;
+}
+
+static void level_generic_free()
+{
+    if (level_context_generic != NULL)
+    {
+        free(level_context_generic);
+        level_context_generic = NULL;
+    }
+}
+
+static void level_free(Level *level, GameManager *manager, void *context)
+{
+    UNUSED(level);
+    UNUSED(manager);
+    UNUSED(context);
+    level_generic_free();
+}
+
+static void level_alloc_generic_world(Level *level, GameManager *manager, void *context)
+{
+    UNUSED(manager);
+    UNUSED(level);
+    if (!level_context_generic)
+    {
+        FURI_LOG_E("Game", "Generic level context not set");
+        return;
+    }
+    if (!context)
+    {
+        FURI_LOG_E("Game", "Context is NULL");
+        return;
+    }
+    LevelContext *level_context = context;
+    snprintf(level_context->id, sizeof(level_context->id), "%s", level_context_generic->id);
+    level_context->index = level_context_generic->index;
+}
+
+const LevelBehaviour _generic_level = {
+    .alloc = level_alloc_generic_world,
+    .free = level_free,
+    .start = level_start,
+    .stop = NULL,
+    .context_size = sizeof(LevelContext),
+};
+
+const LevelBehaviour *generic_level(const char *id, int index)
+{
+    // free any old context before allocating a new one
+    level_generic_free();
+    level_context_generic = level_generic_alloc(id, index);
+    return &_generic_level;
+}

+ 10 - 0
game/level.h

@@ -0,0 +1,10 @@
+#pragma once
+#include "game.h"
+#include "flip_world.h"
+typedef struct
+{
+    char id[64];
+    int index;
+} LevelContext;
+
+const LevelBehaviour *generic_level(const char *id, int index);

+ 438 - 0
game/world.c

@@ -0,0 +1,438 @@
+#include <game/world.h>
+
+void draw_bounds(Canvas *canvas)
+{
+    // Draw the outer bounds adjusted by camera offset
+    // we draw this last to ensure users can see the bounds
+    canvas_draw_frame(canvas, -camera_x, -camera_y, WORLD_WIDTH, WORLD_HEIGHT);
+}
+
+bool draw_json_world(Level *level, const char *json_data)
+{
+    for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
+    {
+        // 1) Get data array item
+        char *data = get_json_array_value("json_data", i, json_data);
+        if (!data)
+        {
+            // Means we've reached the end of the array
+            break;
+        }
+
+        // 2) Extract all required fields
+        char *icon = get_json_value("icon", data);
+        char *x = get_json_value("x", data);
+        char *y = get_json_value("y", data);
+        char *amount = get_json_value("amount", data);
+        char *horizontal = get_json_value("horizontal", data);
+
+        // 3) Check for any NULL pointers
+        if (!icon || !x || !y || !amount || !horizontal)
+        {
+            FURI_LOG_E("Game", "Failed Data: %s", data);
+
+            // Free everything carefully
+            if (data)
+                free(data);
+            if (icon)
+                free(icon);
+            if (x)
+                free(x);
+            if (y)
+                free(y);
+            if (amount)
+                free(amount);
+            if (horizontal)
+                free(horizontal);
+
+            level_clear(level);
+            return false;
+        }
+
+        // 4) Get the IconContext
+        IconContext *icon_context = get_icon_context(icon);
+        if (!icon_context)
+        {
+            FURI_LOG_E("Game", "Failed Icon: %s", icon);
+
+            free(data);
+            free(icon);
+            free(x);
+            free(y);
+            free(amount);
+            free(horizontal);
+
+            level_clear(level);
+            return false;
+        }
+
+        // 5) Decide how many icons to spawn
+        int count = atoi(amount);
+        if (count < 2)
+        {
+            // Just one icon
+            spawn_icon(
+                level,
+                icon_context->icon,
+                atoi(x),
+                atoi(y),
+                icon_context->width,
+                icon_context->height);
+        }
+        else
+        {
+            // Spawn multiple in a line
+            bool is_horizontal = (strcmp(horizontal, "true") == 0);
+            spawn_icon_line(
+                level,
+                icon_context->icon,
+                atoi(x),
+                atoi(y),
+                icon_context->width,
+                icon_context->height,
+                count,
+                is_horizontal);
+        }
+
+        // 6) Cleanup
+        free(data);
+        free(icon);
+        free(x);
+        free(y);
+        free(amount);
+        free(horizontal);
+        free(icon_context);
+    }
+    return true;
+}
+
+bool draw_json_world_furi(Level *level, FuriString *json_data)
+{
+    for (int i = 0; i < MAX_WORLD_OBJECTS; i++)
+    {
+        // 1) Get data array item as FuriString
+        FuriString *data = get_json_array_value_furi("json_data", i, json_data);
+        if (!data)
+        {
+            // Means we've reached the end of the array
+            break;
+        }
+
+        // 2) Extract all required fields
+        FuriString *icon = get_json_value_furi("icon", data);
+        FuriString *x = get_json_value_furi("x", data);
+        FuriString *y = get_json_value_furi("y", data);
+        FuriString *amount = get_json_value_furi("amount", data);
+        FuriString *horizontal = get_json_value_furi("horizontal", data);
+
+        // 3) Check for any NULL pointers
+        if (!icon || !x || !y || !amount || !horizontal)
+        {
+            FURI_LOG_E("Game", "Failed Data: %s", furi_string_get_cstr(data));
+
+            if (data)
+                furi_string_free(data);
+            if (icon)
+                furi_string_free(icon);
+            if (x)
+                furi_string_free(x);
+            if (y)
+                furi_string_free(y);
+            if (amount)
+                furi_string_free(amount);
+            if (horizontal)
+                furi_string_free(horizontal);
+
+            level_clear(level);
+            return false;
+        }
+
+        // 4) Get the IconContext from a FuriString
+        IconContext *icon_context = get_icon_context_furi(icon);
+        if (!icon_context)
+        {
+            FURI_LOG_E("Game", "Failed Icon: %s", furi_string_get_cstr(icon));
+
+            furi_string_free(data);
+            furi_string_free(icon);
+            furi_string_free(x);
+            furi_string_free(y);
+            furi_string_free(amount);
+            furi_string_free(horizontal);
+
+            level_clear(level);
+            return false;
+        }
+
+        // 5) Decide how many icons to spawn
+        int count = atoi(furi_string_get_cstr(amount));
+        if (count < 2)
+        {
+            // Just one icon
+            spawn_icon(
+                level,
+                icon_context->icon,
+                atoi(furi_string_get_cstr(x)),
+                atoi(furi_string_get_cstr(y)),
+                icon_context->width,
+                icon_context->height);
+        }
+        else
+        {
+            // Spawn multiple in a line
+            bool is_horizontal = (furi_string_cmp(horizontal, "true") == 0);
+            spawn_icon_line(
+                level,
+                icon_context->icon,
+                atoi(furi_string_get_cstr(x)),
+                atoi(furi_string_get_cstr(y)),
+                icon_context->width,
+                icon_context->height,
+                count,
+                is_horizontal);
+        }
+
+        // 6) Clean up after every iteration
+        furi_string_free(data);
+        furi_string_free(icon);
+        furi_string_free(x);
+        furi_string_free(y);
+        furi_string_free(amount);
+        furi_string_free(horizontal);
+        free(icon_context);
+    }
+    return true;
+}
+
+void draw_tree_world(Level *level)
+{
+    IconContext *tree_icon = get_icon_context("tree");
+    if (!tree_icon)
+    {
+        FURI_LOG_E("Game", "Failed to get tree icon context");
+        return;
+    }
+    // Spawn two full left/up tree lines
+    for (int i = 0; i < 2; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+        // Vertical line of 11 icons
+        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 2, tree_icon->width, tree_icon->height, 11, false);
+    }
+
+    // Spawn two full down tree lines
+    for (int i = 9; i < 11; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+    }
+
+    // Spawn two full right tree lines
+    for (int i = 20; i < 22; i++)
+    {
+        // Vertical line of 8 icons starting further down (y=50)
+        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 50, tree_icon->width, tree_icon->height, 8, false);
+    }
+
+    // Labyrinth lines
+    // Third line (14 left, then a gap, then 3 middle)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 2 * 17, tree_icon->width, tree_icon->height, 14, true);
+    spawn_icon_line(level, tree_icon->icon, 5 + 16 * 17, 2 + 2 * 17, tree_icon->width, tree_icon->height, 3, true);
+
+    // Fourth line (3 left, 6 middle, 4 right)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 3 * 17, tree_icon->width, tree_icon->height, 3, true);           // 3 left
+    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 3 * 17, tree_icon->width, tree_icon->height, 6, true);  // 6 middle
+    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 3 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
+
+    // Fifth line (6 left, 7 middle)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 4 * 17, tree_icon->width, tree_icon->height, 6, true);
+    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 4 * 17, tree_icon->width, tree_icon->height, 7, true);
+
+    // Sixth line (5 left, 3 middle, 7 right)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 5 * 17, tree_icon->width, tree_icon->height, 5, true);           // 5 left
+    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 5 * 17, tree_icon->width, tree_icon->height, 3, true);  // 3 middle
+    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 5 * 17, tree_icon->width, tree_icon->height, 7, true); // 7 right
+
+    // Seventh line (0 left, 7 middle, 4 right)
+    spawn_icon_line(level, tree_icon->icon, 5 + 6 * 17, 2 + 6 * 17, tree_icon->width, tree_icon->height, 7, true);  // 7 middle
+    spawn_icon_line(level, tree_icon->icon, 5 + 14 * 17, 2 + 6 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
+
+    // Eighth line (4 left, 3 middle, 4 right)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 7 * 17, tree_icon->width, tree_icon->height, 4, true);           // 4 left
+    spawn_icon_line(level, tree_icon->icon, 5 + 7 * 17, 2 + 7 * 17, tree_icon->width, tree_icon->height, 3, true);  // 3 middle
+    spawn_icon_line(level, tree_icon->icon, 5 + 15 * 17, 2 + 7 * 17, tree_icon->width, tree_icon->height, 4, true); // 4 right
+
+    // Ninth line (3 left, 1 middle, 3 right)
+    spawn_icon_line(level, tree_icon->icon, 5, 2 + 8 * 17, tree_icon->width, tree_icon->height, 3, true);           // 3 left
+    spawn_icon_line(level, tree_icon->icon, 5 + 5 * 17, 2 + 8 * 17, tree_icon->width, tree_icon->height, 1, true);  // 1 middle
+    spawn_icon_line(level, tree_icon->icon, 5 + 11 * 17, 2 + 8 * 17, tree_icon->width, tree_icon->height, 3, true); // 3 right
+
+    free(tree_icon);
+}
+
+void draw_town_world(Level *level)
+{
+    // define all the icons
+    IconContext *house_icon = get_icon_context("house");
+    IconContext *fence_icon = get_icon_context("fence");
+    IconContext *fence_end_icon = get_icon_context("fence_end");
+    IconContext *plant_icon = get_icon_context("plant");
+    IconContext *flower_icon = get_icon_context("flower");
+    IconContext *man_icon = get_icon_context("man");
+    IconContext *woman_icon = get_icon_context("woman");
+    IconContext *lake_top_left_icon = get_icon_context("lake_top_left");
+    IconContext *lake_top_icon = get_icon_context("lake_top");
+    IconContext *lake_top_right_icon = get_icon_context("lake_top_right");
+    IconContext *lake_left_icon = get_icon_context("lake_left");
+    IconContext *lake_right_icon = get_icon_context("lake_right");
+    IconContext *lake_bottom_left_icon = get_icon_context("lake_bottom_left");
+    IconContext *lake_bottom_icon = get_icon_context("lake_bottom");
+    IconContext *lake_bottom_right_icon = get_icon_context("lake_bottom_right");
+    IconContext *tree_icon = get_icon_context("tree");
+
+    // check if any of the icons are NULL
+    if (!house_icon || !fence_icon || !fence_end_icon || !plant_icon || !flower_icon ||
+        !man_icon || !woman_icon || !lake_top_left_icon || !lake_top_icon || !lake_top_right_icon ||
+        !lake_left_icon || !lake_right_icon || !lake_bottom_left_icon || !lake_bottom_icon || !lake_bottom_right_icon || !tree_icon)
+    {
+        FURI_LOG_E("Game", "Failed to get icon context");
+        return;
+    }
+
+    // house-fence group 1
+    spawn_icon(level, house_icon->icon, 148, 36, house_icon->width, house_icon->height);
+    spawn_icon(level, fence_icon->icon, 148, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_icon->icon, 164, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_end_icon->icon, 180, 72, fence_end_icon->width, fence_end_icon->height);
+
+    // house-fence group 4 (the left of group 1)
+    spawn_icon(level, house_icon->icon, 96, 36, house_icon->width, house_icon->height);
+    spawn_icon(level, fence_icon->icon, 96, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_icon->icon, 110, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_end_icon->icon, 126, 72, fence_end_icon->width, fence_end_icon->height);
+
+    // house-fence group 5 (the left of group 4)
+    spawn_icon(level, house_icon->icon, 40, 36, house_icon->width, house_icon->height);
+    spawn_icon(level, fence_icon->icon, 40, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_icon->icon, 56, 72, fence_icon->width, fence_icon->height);
+    spawn_icon(level, fence_end_icon->icon, 72, 72, fence_end_icon->width, fence_end_icon->height);
+
+    // line of fences on the 8th row (using spawn_icon_line)
+    spawn_icon_line(level, fence_icon->icon, 8, 100, fence_icon->width, fence_icon->height, 10, true);
+
+    // plants spaced out underneath the fences
+    spawn_icon_line(level, plant_icon->icon, 40, 110, plant_icon->width, plant_icon->height, 6, true);
+    spawn_icon_line(level, flower_icon->icon, 40, 140, flower_icon->width, flower_icon->height, 6, true);
+
+    // man and woman
+    spawn_icon(level, man_icon->icon, 156, 110, man_icon->width, man_icon->height);
+    spawn_icon(level, woman_icon->icon, 164, 110, woman_icon->width, woman_icon->height);
+
+    // lake
+    // Top row
+    spawn_icon(level, lake_top_left_icon->icon, 240, 52, lake_top_left_icon->width, lake_top_left_icon->height);
+    spawn_icon(level, lake_top_icon->icon, 264, 52, lake_top_icon->width, lake_top_icon->height);
+    spawn_icon(level, lake_top_right_icon->icon, 295, 52, lake_top_right_icon->width, lake_top_right_icon->height);
+
+    // Middle row
+    spawn_icon(level, lake_left_icon->icon, 231, 74, lake_left_icon->width, lake_left_icon->height);
+    spawn_icon(level, lake_right_icon->icon, 317, 74, lake_right_icon->width, lake_right_icon->height);
+
+    // Bottom row
+    spawn_icon(level, lake_bottom_left_icon->icon, 240, 105, lake_bottom_left_icon->width, lake_bottom_left_icon->height);
+    spawn_icon(level, lake_bottom_icon->icon, 264, 124, lake_bottom_icon->width, lake_bottom_icon->height);
+    spawn_icon(level, lake_bottom_right_icon->icon, 295, 105, lake_bottom_right_icon->width, lake_bottom_right_icon->height);
+
+    // Spawn two full left/up tree lines
+    for (int i = 0; i < 2; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+        // Vertical line of 11 icons
+        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 2, tree_icon->width, tree_icon->height, 11, false);
+    }
+
+    // Spawn two full down tree lines
+    for (int i = 9; i < 11; i++)
+    {
+        // Horizontal line of 22 icons
+        spawn_icon_line(level, tree_icon->icon, 5, 2 + i * 17, tree_icon->width, tree_icon->height, 22, true);
+    }
+
+    // Spawn two full right tree lines
+    for (int i = 20; i < 22; i++)
+    {
+        // Vertical line of 8 icons starting further down (y=50)
+        spawn_icon_line(level, tree_icon->icon, 5 + i * 17, 50, tree_icon->width, tree_icon->height, 8, false);
+    }
+    free(house_icon);
+    free(fence_icon);
+    free(fence_end_icon);
+    free(plant_icon);
+    free(flower_icon);
+    free(man_icon);
+    free(woman_icon);
+    free(lake_top_left_icon);
+    free(lake_top_icon);
+    free(lake_top_right_icon);
+    free(lake_left_icon);
+    free(lake_right_icon);
+    free(lake_bottom_left_icon);
+    free(lake_bottom_icon);
+    free(lake_bottom_right_icon);
+    free(tree_icon);
+}
+
+FuriString *fetch_world(const char *name)
+{
+    if (!name)
+    {
+        FURI_LOG_E("Game", "World name is NULL");
+        return NULL;
+    }
+    if (!app_instance)
+    {
+        // as long as the game is running, app_instance should be non-NULL
+        FURI_LOG_E("Game", "App instance is NULL");
+        return NULL;
+    }
+
+    if (!flipper_http_init(flipper_http_rx_callback, app_instance))
+    {
+        FURI_LOG_E("Game", "Failed to initialize HTTP");
+        return NULL;
+    }
+    char url[256];
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/get/world/%s/", name);
+    snprintf(fhttp.file_path, sizeof(fhttp.file_path), STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/%s.json", name);
+    fhttp.save_received_data = true;
+    if (!flipper_http_get_request_with_headers(url, "{\"Content-Type\": \"application/json\"}"))
+    {
+        FURI_LOG_E("Game", "Failed to send HTTP request");
+        flipper_http_deinit();
+        return NULL;
+    }
+    fhttp.state = RECEIVING;
+    furi_timer_start(fhttp.get_timeout_timer, TIMEOUT_DURATION_TICKS);
+    while (fhttp.state == RECEIVING && furi_timer_is_running(fhttp.get_timeout_timer) > 0)
+    {
+        // Wait for the request to be received
+        furi_delay_ms(100);
+    }
+    furi_timer_stop(fhttp.get_timeout_timer);
+    if (fhttp.state != IDLE)
+    {
+        FURI_LOG_E("Game", "Failed to receive world data");
+        flipper_http_deinit();
+        return NULL;
+    }
+    flipper_http_deinit();
+    FuriString *returned_data = load_furi_world(name);
+    if (!returned_data)
+    {
+        FURI_LOG_E("Game", "Failed to load world data from file");
+        return NULL;
+    }
+    return returned_data;
+}

+ 19 - 0
game/world.h

@@ -0,0 +1,19 @@
+#pragma once
+#include <game/draw.h>
+// Screen size
+#define SCREEN_WIDTH 128
+#define SCREEN_HEIGHT 64
+
+// World size (3x3)
+#define WORLD_WIDTH 384
+#define WORLD_HEIGHT 192
+
+// Maximum number of world objects
+#define MAX_WORLD_OBJECTS 100
+
+void draw_bounds(Canvas *canvas);
+void draw_tree_world(Level *level);
+void draw_town_world(Level *level);
+bool draw_json_world(Level *level, const char *json_data);
+bool draw_json_world_furi(Level *level, FuriString *json_data);
+FuriString *fetch_world(const char *name);

+ 85 - 46
jsmn/jsmn.c

@@ -7,8 +7,6 @@
  */
  */
 
 
 #include <jsmn/jsmn.h>
 #include <jsmn/jsmn.h>
-#include <stdlib.h>
-#include <string.h>
 
 
 /**
 /**
  * Allocates a fresh unused token from the token pool.
  * Allocates a fresh unused token from the token pool.
@@ -448,14 +446,14 @@ int jsoneq(const char *json, jsmntok_t *tok, const char *s)
 }
 }
 
 
 // Return the value of the key in the JSON data
 // Return the value of the key in the JSON data
-char *get_json_value(char *key, char *json_data, uint32_t max_tokens)
+char *get_json_value(char *key, const char *json_data)
 {
 {
     // Parse the JSON feed
     // Parse the JSON feed
     if (json_data != NULL)
     if (json_data != NULL)
     {
     {
         jsmn_parser parser;
         jsmn_parser parser;
         jsmn_init(&parser);
         jsmn_init(&parser);
-
+        uint32_t max_tokens = json_token_count(json_data);
         // Allocate tokens array on the heap
         // Allocate tokens array on the heap
         jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
         jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
         if (tokens == NULL)
         if (tokens == NULL)
@@ -510,26 +508,73 @@ char *get_json_value(char *key, char *json_data, uint32_t max_tokens)
     {
     {
         FURI_LOG_E("JSMM.H", "JSON data is NULL");
         FURI_LOG_E("JSMM.H", "JSON data is NULL");
     }
     }
-    FURI_LOG_E("JSMM.H", "Failed to find the key in the JSON.");
+    char warning[128];
+    snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key);
+    FURI_LOG_E("JSMM.H", warning);
     return NULL; // Return NULL if something goes wrong
     return NULL; // Return NULL if something goes wrong
 }
 }
 
 
-// Revised get_json_array_value function
-char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens)
+// Helper function to skip a token and all its descendants.
+// Returns the index of the next token after skipping this one.
+// On error or out of bounds, returns -1.
+static int skip_token(const jsmntok_t *tokens, int start, int total)
 {
 {
-    // Retrieve the array string for the given key
-    char *array_str = get_json_value(key, json_data, max_tokens);
+    if (start < 0 || start >= total)
+        return -1;
+
+    int i = start;
+    if (tokens[i].type == JSMN_OBJECT)
+    {
+        // For an object: size is number of key-value pairs
+        int pairs = tokens[i].size;
+        i++; // move to first key-value pair
+        for (int p = 0; p < pairs; p++)
+        {
+            // skip key (primitive/string)
+            i++;
+            if (i >= total)
+                return -1;
+            // skip value (which could be object/array and must be skipped recursively)
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i; // i is now just past the object
+    }
+    else if (tokens[i].type == JSMN_ARRAY)
+    {
+        // For an array: size is number of elements
+        int elems = tokens[i].size;
+        i++; // move to first element
+        for (int e = 0; e < elems; e++)
+        {
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i; // i is now just past the array
+    }
+    else
+    {
+        // Primitive or string token, just skip it
+        return i + 1;
+    }
+}
+
+// Revised get_json_array_value
+char *get_json_array_value(char *key, uint32_t index, const char *json_data)
+{
+    // Always extract the full array each time from the original json_data
+    char *array_str = get_json_value(key, json_data);
     if (array_str == NULL)
     if (array_str == NULL)
     {
     {
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         return NULL;
         return NULL;
     }
     }
+    uint32_t max_tokens = json_token_count(array_str);
 
 
-    // Initialize the JSON parser
     jsmn_parser parser;
     jsmn_parser parser;
     jsmn_init(&parser);
     jsmn_init(&parser);
-
-    // Allocate memory for JSON tokens
     jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
     jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
     if (tokens == NULL)
     if (tokens == NULL)
     {
     {
@@ -538,7 +583,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
         return NULL;
     }
     }
 
 
-    // Parse the JSON array
     int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens);
     int ret = jsmn_parse(&parser, array_str, strlen(array_str), tokens, max_tokens);
     if (ret < 0)
     if (ret < 0)
     {
     {
@@ -548,7 +592,6 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
         return NULL;
     }
     }
 
 
-    // Ensure the root element is an array
     if (ret < 1 || tokens[0].type != JSMN_ARRAY)
     if (ret < 1 || tokens[0].type != JSMN_ARRAY)
     {
     {
         FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key);
         FURI_LOG_E("JSMM.H", "Value for key '%s' is not an array.", key);
@@ -557,50 +600,33 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
         return NULL;
     }
     }
 
 
-    // Check if the index is within bounds
     if (index >= (uint32_t)tokens[0].size)
     if (index >= (uint32_t)tokens[0].size)
     {
     {
-        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %d.", (unsigned long)index, tokens[0].size);
+        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
         free(tokens);
         free(tokens);
         free(array_str);
         free(array_str);
         return NULL;
         return NULL;
     }
     }
 
 
-    // Locate the token corresponding to the desired array element
-    int current_token = 1; // Start after the array token
+    // Find the index-th element: start from token[1], which is the first element
+    int elem_token = 1;
     for (uint32_t i = 0; i < index; i++)
     for (uint32_t i = 0; i < index; i++)
     {
     {
-        if (tokens[current_token].type == JSMN_OBJECT)
-        {
-            // For objects, skip all key-value pairs
-            current_token += 1 + 2 * tokens[current_token].size;
-        }
-        else if (tokens[current_token].type == JSMN_ARRAY)
-        {
-            // For nested arrays, skip all elements
-            current_token += 1 + tokens[current_token].size;
-        }
-        else
+        elem_token = skip_token(tokens, elem_token, ret);
+        if (elem_token == -1 || elem_token >= ret)
         {
         {
-            // For primitive types, simply move to the next token
-            current_token += 1;
-        }
-
-        // Safety check to prevent out-of-bounds
-        if (current_token >= ret)
-        {
-            FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+            FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i);
             free(tokens);
             free(tokens);
             free(array_str);
             free(array_str);
             return NULL;
             return NULL;
         }
         }
     }
     }
 
 
-    // Extract the array element
-    jsmntok_t element = tokens[current_token];
+    // Now elem_token should point to the token of the requested element
+    jsmntok_t element = tokens[elem_token];
     int length = element.end - element.start;
     int length = element.end - element.start;
     char *value = malloc(length + 1);
     char *value = malloc(length + 1);
-    if (value == NULL)
+    if (!value)
     {
     {
         FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element.");
         FURI_LOG_E("JSMM.H", "Failed to allocate memory for array element.");
         free(tokens);
         free(tokens);
@@ -608,11 +634,9 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
         return NULL;
         return NULL;
     }
     }
 
 
-    // Copy the element value to a new string
     strncpy(value, array_str + element.start, length);
     strncpy(value, array_str + element.start, length);
-    value[length] = '\0'; // Null-terminate the string
+    value[length] = '\0';
 
 
-    // Clean up
     free(tokens);
     free(tokens);
     free(array_str);
     free(array_str);
 
 
@@ -620,16 +644,16 @@ char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t
 }
 }
 
 
 // Revised get_json_array_values function with correct token skipping
 // Revised get_json_array_values function with correct token skipping
-char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values)
+char **get_json_array_values(char *key, char *json_data, int *num_values)
 {
 {
     // Retrieve the array string for the given key
     // Retrieve the array string for the given key
-    char *array_str = get_json_value(key, json_data, max_tokens);
+    char *array_str = get_json_value(key, json_data);
     if (array_str == NULL)
     if (array_str == NULL)
     {
     {
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         FURI_LOG_E("JSMM.H", "Failed to get array for key: %s", key);
         return NULL;
         return NULL;
     }
     }
-
+    uint32_t max_tokens = json_token_count(array_str);
     // Initialize the JSON parser
     // Initialize the JSON parser
     jsmn_parser parser;
     jsmn_parser parser;
     jsmn_init(&parser);
     jsmn_init(&parser);
@@ -745,3 +769,18 @@ char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, in
     free(array_str);
     free(array_str);
     return values;
     return values;
 }
 }
+
+int json_token_count(const char *json)
+{
+    if (json == NULL)
+    {
+        return JSMN_ERROR_INVAL;
+    }
+
+    jsmn_parser parser;
+    jsmn_init(&parser);
+
+    // Pass NULL for tokens and 0 for num_tokens to get the token count only
+    int ret = jsmn_parse(&parser, json, strlen(json), NULL, 0);
+    return ret; // If ret >= 0, it represents the number of tokens needed.
+}

+ 6 - 58
jsmn/jsmn.h

@@ -17,6 +17,7 @@
 #define JSMN_H
 #define JSMN_H
 
 
 #include <stddef.h>
 #include <stddef.h>
+#include <jsmn/jsmn_h.h>
 
 
 #ifdef __cplusplus
 #ifdef __cplusplus
 extern "C"
 extern "C"
@@ -28,61 +29,6 @@ extern "C"
 #else
 #else
 #define JSMN_API extern
 #define JSMN_API extern
 #endif
 #endif
-
-    /**
-     * JSON type identifier. Basic types are:
-     * 	o Object
-     * 	o Array
-     * 	o String
-     * 	o Other primitive: number, boolean (true/false) or null
-     */
-    typedef enum
-    {
-        JSMN_UNDEFINED = 0,
-        JSMN_OBJECT = 1 << 0,
-        JSMN_ARRAY = 1 << 1,
-        JSMN_STRING = 1 << 2,
-        JSMN_PRIMITIVE = 1 << 3
-    } jsmntype_t;
-
-    enum jsmnerr
-    {
-        /* Not enough tokens were provided */
-        JSMN_ERROR_NOMEM = -1,
-        /* Invalid character inside JSON string */
-        JSMN_ERROR_INVAL = -2,
-        /* The string is not a full JSON packet, more bytes expected */
-        JSMN_ERROR_PART = -3
-    };
-
-    /**
-     * JSON token description.
-     * type		type (object, array, string etc.)
-     * start	start position in JSON data string
-     * end		end position in JSON data string
-     */
-    typedef struct
-    {
-        jsmntype_t type;
-        int start;
-        int end;
-        int size;
-#ifdef JSMN_PARENT_LINKS
-        int parent;
-#endif
-    } jsmntok_t;
-
-    /**
-     * JSON parser. Contains an array of token blocks available. Also stores
-     * the string being parsed now and current position in that string.
-     */
-    typedef struct
-    {
-        unsigned int pos;     /* offset in the JSON string */
-        unsigned int toknext; /* next token to allocate */
-        int toksuper;         /* superior token node, e.g. parent object or array */
-    } jsmn_parser;
-
     /**
     /**
      * Create JSON parser over an array of tokens
      * Create JSON parser over an array of tokens
      */
      */
@@ -122,11 +68,13 @@ char *jsmn(const char *key, const char *value);
 int jsoneq(const char *json, jsmntok_t *tok, const char *s);
 int jsoneq(const char *json, jsmntok_t *tok, const char *s);
 
 
 // Return the value of the key in the JSON data
 // Return the value of the key in the JSON data
-char *get_json_value(char *key, char *json_data, uint32_t max_tokens);
+char *get_json_value(char *key, const char *json_data);
 
 
 // Revised get_json_array_value function
 // Revised get_json_array_value function
-char *get_json_array_value(char *key, uint32_t index, char *json_data, uint32_t max_tokens);
+char *get_json_array_value(char *key, uint32_t index, const char *json_data);
 
 
 // Revised get_json_array_values function with correct token skipping
 // Revised get_json_array_values function with correct token skipping
-char **get_json_array_values(char *key, char *json_data, uint32_t max_tokens, int *num_values);
+char **get_json_array_values(char *key, char *json_data, int *num_values);
+
+int json_token_count(const char *json);
 #endif /* JB_JSMN_EDIT */
 #endif /* JB_JSMN_EDIT */

+ 722 - 0
jsmn/jsmn_furi.c

@@ -0,0 +1,722 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010 Serge Zaitsev
+ *
+ * [License text continues...]
+ */
+
+#include <jsmn/jsmn_furi.h>
+
+// Forward declarations of helper functions
+static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s);
+static int skip_token(const jsmntok_t *tokens, int start, int total);
+
+/**
+ * Allocates a fresh unused token from the token pool.
+ */
+static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens,
+                                   const size_t num_tokens)
+{
+    if (parser->toknext >= num_tokens)
+    {
+        return NULL;
+    }
+    jsmntok_t *tok = &tokens[parser->toknext++];
+    tok->start = tok->end = -1;
+    tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+    tok->parent = -1;
+#endif
+    return tok;
+}
+
+/**
+ * Fills token type and boundaries.
+ */
+static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type,
+                            const int start, const int end)
+{
+    token->type = type;
+    token->start = start;
+    token->end = end;
+    token->size = 0;
+}
+
+/**
+ * Fills next available token with JSON primitive.
+ * Now uses FuriString to access characters.
+ */
+static int jsmn_parse_primitive(jsmn_parser *parser, const FuriString *js,
+                                jsmntok_t *tokens, const size_t num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int start = parser->pos;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        switch (c)
+        {
+#ifndef JSMN_STRICT
+        case ':':
+#endif
+        case '\t':
+        case '\r':
+        case '\n':
+        case ' ':
+        case ',':
+        case ']':
+        case '}':
+            goto found;
+        default:
+            break;
+        }
+        if (c < 32 || c >= 127)
+        {
+            parser->pos = start;
+            return JSMN_ERROR_INVAL;
+        }
+    }
+
+#ifdef JSMN_STRICT
+    // In strict mode primitive must be followed by a comma/object/array
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+#endif
+
+found:
+    if (tokens == NULL)
+    {
+        parser->pos--;
+        return 0;
+    }
+    jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+    if (token == NULL)
+    {
+        parser->pos = start;
+        return JSMN_ERROR_NOMEM;
+    }
+    jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+    token->parent = parser->toksuper;
+#endif
+    parser->pos--;
+    return 0;
+}
+
+/**
+ * Fills next token with JSON string.
+ * Now uses FuriString to access characters.
+ */
+static int jsmn_parse_string(jsmn_parser *parser, const FuriString *js,
+                             jsmntok_t *tokens, const size_t num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int start = parser->pos;
+    parser->pos++;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        if (c == '\"')
+        {
+            if (tokens == NULL)
+            {
+                return 0;
+            }
+            jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+            if (token == NULL)
+            {
+                parser->pos = start;
+                return JSMN_ERROR_NOMEM;
+            }
+            jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+            token->parent = parser->toksuper;
+#endif
+            return 0;
+        }
+
+        if (c == '\\' && (parser->pos + 1) < len)
+        {
+            parser->pos++;
+            char esc = furi_string_get_char(js, parser->pos);
+            switch (esc)
+            {
+            case '\"':
+            case '/':
+            case '\\':
+            case 'b':
+            case 'f':
+            case 'r':
+            case 'n':
+            case 't':
+                break;
+            case 'u':
+            {
+                parser->pos++;
+                for (int i = 0; i < 4 && parser->pos < len; i++)
+                {
+                    char hex = furi_string_get_char(js, parser->pos);
+                    if (!((hex >= '0' && hex <= '9') ||
+                          (hex >= 'A' && hex <= 'F') ||
+                          (hex >= 'a' && hex <= 'f')))
+                    {
+                        parser->pos = start;
+                        return JSMN_ERROR_INVAL;
+                    }
+                    parser->pos++;
+                }
+                parser->pos--;
+                break;
+            }
+            default:
+                parser->pos = start;
+                return JSMN_ERROR_INVAL;
+            }
+        }
+    }
+    parser->pos = start;
+    return JSMN_ERROR_PART;
+}
+
+/**
+ * Create JSON parser
+ */
+void jsmn_init_furi(jsmn_parser *parser)
+{
+    parser->pos = 0;
+    parser->toknext = 0;
+    parser->toksuper = -1;
+}
+
+/**
+ * Parse JSON string and fill tokens.
+ * Now uses FuriString for the input JSON.
+ */
+int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js,
+                    jsmntok_t *tokens, const unsigned int num_tokens)
+{
+    size_t len = furi_string_size(js);
+    int r;
+    int i;
+    int count = parser->toknext;
+
+    for (; parser->pos < len; parser->pos++)
+    {
+        char c = furi_string_get_char(js, parser->pos);
+        jsmntype_t type;
+
+        switch (c)
+        {
+        case '{':
+        case '[':
+        {
+            count++;
+            if (tokens == NULL)
+            {
+                break;
+            }
+            jsmntok_t *token = jsmn_alloc_token(parser, tokens, num_tokens);
+            if (token == NULL)
+                return JSMN_ERROR_NOMEM;
+            if (parser->toksuper != -1)
+            {
+                jsmntok_t *t = &tokens[parser->toksuper];
+#ifdef JSMN_STRICT
+                if (t->type == JSMN_OBJECT)
+                    return JSMN_ERROR_INVAL;
+#endif
+                t->size++;
+#ifdef JSMN_PARENT_LINKS
+                token->parent = parser->toksuper;
+#endif
+            }
+            token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+            token->start = parser->pos;
+            parser->toksuper = parser->toknext - 1;
+            break;
+        }
+        case '}':
+        case ']':
+            if (tokens == NULL)
+            {
+                break;
+            }
+            type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+            if (parser->toknext < 1)
+            {
+                return JSMN_ERROR_INVAL;
+            }
+            {
+                jsmntok_t *token = &tokens[parser->toknext - 1];
+                for (;;)
+                {
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        if (token->type != type)
+                            return JSMN_ERROR_INVAL;
+                        token->end = parser->pos + 1;
+                        parser->toksuper = token->parent;
+                        break;
+                    }
+                    if (token->parent == -1)
+                    {
+                        if (token->type != type || parser->toksuper == -1)
+                        {
+                            return JSMN_ERROR_INVAL;
+                        }
+                        break;
+                    }
+                    token = &tokens[token->parent];
+                }
+            }
+#else
+            {
+                jsmntok_t *token;
+                for (i = parser->toknext - 1; i >= 0; i--)
+                {
+                    token = &tokens[i];
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        if (token->type != type)
+                            return JSMN_ERROR_INVAL;
+                        parser->toksuper = -1;
+                        token->end = parser->pos + 1;
+                        break;
+                    }
+                }
+                if (i == -1)
+                    return JSMN_ERROR_INVAL;
+                for (; i >= 0; i--)
+                {
+                    token = &tokens[i];
+                    if (token->start != -1 && token->end == -1)
+                    {
+                        parser->toksuper = i;
+                        break;
+                    }
+                }
+            }
+#endif
+            break;
+        case '\"':
+            r = jsmn_parse_string(parser, js, tokens, num_tokens);
+            if (r < 0)
+                return r;
+            count++;
+            if (parser->toksuper != -1 && tokens != NULL)
+            {
+                tokens[parser->toksuper].size++;
+            }
+            break;
+        case '\t':
+        case '\r':
+        case '\n':
+        case ' ':
+            // Whitespace - ignore
+            break;
+        case ':':
+            parser->toksuper = parser->toknext - 1;
+            break;
+        case ',':
+            if (tokens != NULL && parser->toksuper != -1 &&
+                tokens[parser->toksuper].type != JSMN_ARRAY &&
+                tokens[parser->toksuper].type != JSMN_OBJECT)
+            {
+#ifdef JSMN_PARENT_LINKS
+                parser->toksuper = tokens[parser->toksuper].parent;
+#else
+                for (i = parser->toknext - 1; i >= 0; i--)
+                {
+                    if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT)
+                    {
+                        if (tokens[i].start != -1 && tokens[i].end == -1)
+                        {
+                            parser->toksuper = i;
+                            break;
+                        }
+                    }
+                }
+#endif
+            }
+            break;
+#ifdef JSMN_STRICT
+        case '-':
+        case '0':
+        case '1':
+        case '2':
+        case '3':
+        case '4':
+        case '5':
+        case '6':
+        case '7':
+        case '8':
+        case '9':
+        case 't':
+        case 'f':
+        case 'n':
+            if (tokens != NULL && parser->toksuper != -1)
+            {
+                const jsmntok_t *t = &tokens[parser->toksuper];
+                if (t->type == JSMN_OBJECT ||
+                    (t->type == JSMN_STRING && t->size != 0))
+                {
+                    return JSMN_ERROR_INVAL;
+                }
+            }
+#else
+        default:
+#endif
+            r = jsmn_parse_primitive(parser, js, tokens, num_tokens);
+            if (r < 0)
+                return r;
+            count++;
+            if (parser->toksuper != -1 && tokens != NULL)
+            {
+                tokens[parser->toksuper].size++;
+            }
+            break;
+#ifdef JSMN_STRICT
+        default:
+            return JSMN_ERROR_INVAL;
+#endif
+        }
+    }
+
+    if (tokens != NULL)
+    {
+        for (i = parser->toknext - 1; i >= 0; i--)
+        {
+            if (tokens[i].start != -1 && tokens[i].end == -1)
+            {
+                return JSMN_ERROR_PART;
+            }
+        }
+    }
+
+    return count;
+}
+
+// The rest of your code (e.g., get_json_value_furi, get_json_array_value_furi, etc.)
+// remains unchanged and can still rely on these updated parsing functions.
+
+// Helper function to create a JSON object: {"key":"value"}
+FuriString *jsmn_create_object(const FuriString *key, const FuriString *value)
+{
+    FuriString *result = furi_string_alloc();
+    furi_string_printf(result, "{\"%s\":\"%s\"}",
+                       furi_string_get_cstr(key),
+                       furi_string_get_cstr(value));
+    return result; // Caller responsible for furi_string_free
+}
+
+// Helper function to compare JSON keys
+static int jsoneq_furi(const FuriString *json, jsmntok_t *tok, const FuriString *s)
+{
+    size_t s_len = furi_string_size(s);
+    size_t tok_len = tok->end - tok->start;
+
+    if (tok->type != JSMN_STRING)
+        return -1;
+    if (s_len != tok_len)
+        return -1;
+
+    FuriString *sub = furi_string_alloc_set(json);
+    furi_string_mid(sub, tok->start, tok_len);
+
+    int res = furi_string_cmp(sub, s);
+    furi_string_free(sub);
+
+    return (res == 0) ? 0 : -1;
+}
+
+// Skip a token and its descendants
+static int skip_token(const jsmntok_t *tokens, int start, int total)
+{
+    if (start < 0 || start >= total)
+        return -1;
+
+    int i = start;
+    if (tokens[i].type == JSMN_OBJECT)
+    {
+        int pairs = tokens[i].size;
+        i++;
+        for (int p = 0; p < pairs; p++)
+        {
+            i++; // skip key
+            if (i >= total)
+                return -1;
+            i = skip_token(tokens, i, total); // skip value
+            if (i == -1)
+                return -1;
+        }
+        return i;
+    }
+    else if (tokens[i].type == JSMN_ARRAY)
+    {
+        int elems = tokens[i].size;
+        i++;
+        for (int e = 0; e < elems; e++)
+        {
+            i = skip_token(tokens, i, total);
+            if (i == -1)
+                return -1;
+        }
+        return i;
+    }
+    else
+    {
+        return i + 1;
+    }
+}
+
+/**
+ * Parse JSON and return the value associated with a given char* key.
+ */
+FuriString *get_json_value_furi(const char *key, const FuriString *json_data)
+{
+    if (json_data == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "JSON data is NULL");
+        return NULL;
+    }
+    uint32_t max_tokens = json_token_count_furi(json_data);
+    // Create a temporary FuriString from key
+    FuriString *key_str = furi_string_alloc();
+    furi_string_cat_str(key_str, key);
+
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, json_data, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON: %d", ret);
+        free(tokens);
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    if (ret < 1 || tokens[0].type != JSMN_OBJECT)
+    {
+        FURI_LOG_E("JSMM.H", "Root element is not an object.");
+        free(tokens);
+        furi_string_free(key_str);
+        return NULL;
+    }
+
+    for (int i = 1; i < ret; i++)
+    {
+        if (jsoneq_furi(json_data, &tokens[i], key_str) == 0)
+        {
+            int length = tokens[i + 1].end - tokens[i + 1].start;
+            FuriString *value = furi_string_alloc_set(json_data);
+            furi_string_mid(value, tokens[i + 1].start, length);
+            free(tokens);
+            furi_string_free(key_str);
+            return value;
+        }
+    }
+
+    free(tokens);
+    furi_string_free(key_str);
+    char warning[128];
+    snprintf(warning, sizeof(warning), "Failed to find the key \"%s\" in the JSON.", key);
+    FURI_LOG_E("JSMM.H", warning);
+    return NULL;
+}
+
+/**
+ * Return the value at a given index in a JSON array for a given char* key.
+ */
+FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data)
+{
+    FuriString *array_str = get_json_value_furi(key, json_data);
+    if (array_str == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to get array for key");
+        return NULL;
+    }
+    uint32_t max_tokens = json_token_count_furi(array_str);
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (ret < 1 || tokens[0].type != JSMN_ARRAY)
+    {
+        FURI_LOG_E("JSMM.H", "Value for key is not an array.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (index >= (uint32_t)tokens[0].size)
+    {
+        FURI_LOG_E("JSMM.H", "Index %lu out of bounds for array with size %u.", index, tokens[0].size);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int elem_token = 1;
+    for (uint32_t i = 0; i < index; i++)
+    {
+        elem_token = skip_token(tokens, elem_token, ret);
+        if (elem_token == -1 || elem_token >= ret)
+        {
+            FURI_LOG_E("JSMM.H", "Error skipping tokens to reach element %lu.", i);
+            free(tokens);
+            furi_string_free(array_str);
+            return NULL;
+        }
+    }
+
+    jsmntok_t element = tokens[elem_token];
+    int length = element.end - element.start;
+
+    FuriString *value = furi_string_alloc_set(array_str);
+    furi_string_mid(value, element.start, length);
+
+    free(tokens);
+    furi_string_free(array_str);
+
+    return value;
+}
+
+/**
+ * Extract all object values from a JSON array associated with a given char* key.
+ */
+FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values)
+{
+    *num_values = 0;
+    // Convert key to FuriString and call get_json_value_furi
+    FuriString *array_str = get_json_value_furi(key, json_data);
+    if (array_str == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to get array for key");
+        return NULL;
+    }
+
+    uint32_t max_tokens = json_token_count_furi(array_str);
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    jsmntok_t *tokens = (jsmntok_t *)malloc(sizeof(jsmntok_t) * max_tokens);
+    if (tokens == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int ret = jsmn_parse_furi(&parser, array_str, tokens, max_tokens);
+    if (ret < 0)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to parse JSON array: %d", ret);
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    if (tokens[0].type != JSMN_ARRAY)
+    {
+        FURI_LOG_E("JSMM.H", "Value for key is not an array.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int array_size = tokens[0].size;
+    FuriString **values = (FuriString **)malloc(array_size * sizeof(FuriString *));
+    if (values == NULL)
+    {
+        FURI_LOG_E("JSMM.H", "Failed to allocate memory for array of values.");
+        free(tokens);
+        furi_string_free(array_str);
+        return NULL;
+    }
+
+    int actual_num_values = 0;
+    int current_token = 1;
+    for (int i = 0; i < array_size; i++)
+    {
+        if (current_token >= ret)
+        {
+            FURI_LOG_E("JSMM.H", "Unexpected end of tokens while traversing array.");
+            break;
+        }
+
+        jsmntok_t element = tokens[current_token];
+
+        int length = element.end - element.start;
+        FuriString *value = furi_string_alloc_set(array_str);
+        furi_string_mid(value, element.start, length);
+
+        values[actual_num_values] = value;
+        actual_num_values++;
+
+        // Skip this element and its descendants
+        current_token = skip_token(tokens, current_token, ret);
+        if (current_token == -1)
+        {
+            FURI_LOG_E("JSMM.H", "Error skipping tokens after element %d.", i);
+            break;
+        }
+    }
+
+    *num_values = actual_num_values;
+    if (actual_num_values < array_size)
+    {
+        FuriString **reduced_values = (FuriString **)realloc(values, actual_num_values * sizeof(FuriString *));
+        if (reduced_values != NULL)
+        {
+            values = reduced_values;
+        }
+    }
+
+    free(tokens);
+    furi_string_free(array_str);
+    return values;
+}
+
+uint32_t json_token_count_furi(const FuriString *json)
+{
+    if (json == NULL)
+    {
+        return JSMN_ERROR_INVAL;
+    }
+
+    jsmn_parser parser;
+    jsmn_init_furi(&parser);
+
+    // Pass NULL for tokens and 0 for num_tokens to get the token count only
+    int ret = jsmn_parse_furi(&parser, json, NULL, 0);
+    return ret; // If ret >= 0, it represents the number of tokens needed.
+}

+ 74 - 0
jsmn/jsmn_furi.h

@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2010 Serge Zaitsev
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * [License text continues...]
+ */
+
+#ifndef JSMN_FURI_H
+#define JSMN_FURI_H
+
+#include <jsmn/jsmn_h.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#ifdef JSMN_STATIC
+#define JSMN_API static
+#else
+#define JSMN_API extern
+#endif
+
+    JSMN_API void jsmn_init_furi(jsmn_parser *parser);
+    JSMN_API int jsmn_parse_furi(jsmn_parser *parser, const FuriString *js,
+                                 jsmntok_t *tokens, const unsigned int num_tokens);
+
+#ifndef JSMN_HEADER
+/* Implementation in jsmn_furi.c */
+#endif /* JSMN_HEADER */
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* JSMN_FURI_H */
+
+#ifndef JB_JSMN_FURI_EDIT
+#define JB_JSMN_FURI_EDIT
+
+// Helper function to create a JSON object
+FuriString *jsmn_create_object(const FuriString *key, const FuriString *value);
+
+// Updated signatures to accept const char* key
+FuriString *get_json_value_furi(const char *key, const FuriString *json_data);
+FuriString *get_json_array_value_furi(const char *key, uint32_t index, const FuriString *json_data);
+FuriString **get_json_array_values_furi(const char *key, const FuriString *json_data, int *num_values);
+
+uint32_t json_token_count_furi(const FuriString *json);
+/* Example usage:
+char *json = "{\"key1\":\"value1\",\"key2\":\"value2\"}";
+FuriString *json_data = char_to_furi_string(json);
+if (!json_data)
+{
+    FURI_LOG_E(TAG, "Failed to allocate FuriString");
+    return -1;
+}
+FuriString *value = get_json_value_furi("key1", json_data, json_token_count_furi(json_data));
+if (value)
+{
+    FURI_LOG_I(TAG, "Value: %s", furi_string_get_cstr(value));
+    furi_string_free(value);
+}
+furi_string_free(json_data);
+*/
+#endif /* JB_JSMN_EDIT */

+ 14 - 0
jsmn/jsmn_h.c

@@ -0,0 +1,14 @@
+#include <jsmn/jsmn_h.h>
+FuriString *char_to_furi_string(const char *str)
+{
+    FuriString *furi_str = furi_string_alloc();
+    if (!furi_str)
+    {
+        return NULL;
+    }
+    for (size_t i = 0; i < strlen(str); i++)
+    {
+        furi_string_push_back(furi_str, str[i]);
+    }
+    return furi_str;
+}

+ 41 - 0
jsmn/jsmn_h.h

@@ -0,0 +1,41 @@
+#pragma once
+#include <furi.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+typedef enum
+{
+    JSMN_UNDEFINED = 0,
+    JSMN_OBJECT = 1 << 0,
+    JSMN_ARRAY = 1 << 1,
+    JSMN_STRING = 1 << 2,
+    JSMN_PRIMITIVE = 1 << 3
+} jsmntype_t;
+
+enum jsmnerr
+{
+    JSMN_ERROR_NOMEM = -1,
+    JSMN_ERROR_INVAL = -2,
+    JSMN_ERROR_PART = -3
+};
+
+typedef struct
+{
+    jsmntype_t type;
+    int start;
+    int end;
+    int size;
+#ifdef JSMN_PARENT_LINKS
+    int parent;
+#endif
+} jsmntok_t;
+
+typedef struct
+{
+    unsigned int pos;     /* offset in the JSON string */
+    unsigned int toknext; /* next token to allocate */
+    int toksuper;         /* superior token node, e.g. parent object or array */
+} jsmn_parser;
+
+FuriString *char_to_furi_string(const char *str);

BIN=BIN
sprites/player.png


BIN=BIN
sprites/player_left.png


BIN=BIN
sprites/player_right.png


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio