jblanked 1 год назад
Родитель
Сommit
967ad03c8d

+ 1 - 9
README.md

@@ -56,34 +56,26 @@ If an enemy attacks you, your health decreases by the enemy's strength (attack p
 ## Roadmap
 
 **v0.2**
-
 - Game Mechanics
 - Video Game Module support
 
 **v0.3**
-
 - Stability patch
 
 **v0.4**
-
-- ???
+- New game features
 
 **v0.5**
-
 - ???
 
 **v0.6**
-
 - ???
 
 **v0.7**
-
 - ???
 
 **v0.8**
-
 - Multiplayer support
 
 **v1.0**
-
 - Official release

+ 4 - 2
app.c

@@ -39,7 +39,7 @@ int32_t flip_world_main(void *p)
     while (fhttp->state == INACTIVE && --counter > 0)
     {
         FURI_LOG_D(TAG, "Waiting for PONG");
-        furi_delay_ms(100);
+        furi_delay_ms(100); // this causes a BusFault
     }
 
     flipper_http_free(fhttp);
@@ -49,7 +49,9 @@ int32_t flip_world_main(void *p)
     }
 
     // save app version
-    save_char("app_version", "0.2.1");
+    char app_version[16];
+    snprintf(app_version, sizeof(app_version), "%f", (double)VERSION);
+    save_char("app_version", app_version);
 
     // Run the view dispatcher
     view_dispatcher_run(app->view_dispatcher);

+ 1 - 1
application.fam

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

BIN
assets/01-home.png


+ 18 - 19
assets/CHANGELOG.md

@@ -1,21 +1,20 @@
-## 0.2.1 (2025-01-09)
-- Removed data migration
+**0.3 (2025-01-14)**
+- Added new worlds.
+- Improved memory allocation.
+- Updated API integration to load and save player attributes.
+- Upgraded FlipperHTTP to the latest version.
 
-## 0.2 (2025-01-02)
-Added
-- **Video Game Module Support:** Added support for the Video Game Module (requires FlipperHTTP flash).
-- **Enemies:** Introduced various enemy types to enhance gameplay.
-- **Player Attributes:** Added player health, XP, level, health regeneration, attack, and strength.
-- **Notifications:** Implemented vibration, sound, and LED notifications when a player is attacking or being attacked.
-- **User Interface Enhancements:**: Displayed the player's username above their character and showed the player's health, XP, and level in the bottom left corner of the screen, visible at all times.
+**0.2 (2025-01-02)**
+- Added support for the Video Game Module (requires a FlipperHTTP flash).
+- Introduced various enemy types to enhance gameplay.
+- Added features for player health, XP, level, health regeneration, attack, and strength.
+- Implemented vibration, sound, and LED notifications when a player is attacking or being attacked.
+- Displayed the player's username above their character and showed the player's health, XP, and level in the bottom left corner of the screen at all times.
+- Updated all game icons for improved visual appeal.
+- Upgraded to the latest version of the FlipperHTTP library.
+- Revised toggles in the Game Settings to ensure they work as intended.
+- Improved collision mechanics for more accurate interactions.
+- Updated the default icon representing the player's character.
 
-Changed
-- **Icons:** Updated all game icons for better visual appeal.
-- **Library Update:** Upgraded to the latest version of the FlipperHTTP library.
-- **Game Settings:** Revised toggles in the Game Settings to ensure they work as intended.
-- **Collisions:** Improved collision mechanics for more accurate interactions.
-- **Default Character Icon:** Updated the default icon representing the player's character.
-
-## 0.1 (2024-12-21)
-Added
-- **Initial Release:** Launched the first version of the game with basic features.
+**0.1 (2024-12-21)**
+- Initial release.

+ 1 - 9
assets/README.md

@@ -54,34 +54,26 @@ If an enemy attacks you, your health decreases by the enemy's strength (attack p
 ## Roadmap
 
 **v0.2**
-
 - Game Mechanics
 - Video Game Module support
 
 **v0.3**
-
 - Stability patch
 
 **v0.4**
-
-- ???
+- New game features
 
 **v0.5**
-
 - ???
 
 **v0.6**
-
 - ???
 
 **v0.7**
-
 - ???
 
 **v0.8**
-
 - Multiplayer support
 
 **v1.0**
-
 - Official release

BIN
assets/icon_menu_128x64px.png


BIN
assets/icon_world_change_128x64px.png


+ 49 - 34
callback/callback.c

@@ -1,10 +1,10 @@
 #include <callback/callback.h>
-#include <furi.h>
 #include "engine/engine.h"
 #include "engine/game_engine.h"
 #include "engine/game_manager_i.h"
 #include "engine/level_i.h"
 #include "engine/entity_i.h"
+#include "game/storage.h"
 
 // Below added by Derek Jamison
 // FURI_LOG_DEV will log only during app development. Be sure that Settings/System/Log Device is "LPUART"; so we dont use serial port.
@@ -181,12 +181,13 @@ static void flip_world_view_about_draw_callback(Canvas *canvas, void *model)
 {
     UNUSED(model);
     canvas_clear(canvas);
-    canvas_set_font_custom(canvas, FONT_SIZE_XLARGE);
+    // canvas_set_font_custom(canvas, FONT_SIZE_XLARGE);
     canvas_draw_str(canvas, 0, 10, VERSION_TAG);
-    canvas_set_font_custom(canvas, FONT_SIZE_MEDIUM);
-    canvas_draw_str(canvas, 0, 20, "- @JBlanked @codeallnight");
+    // canvas_set_font_custom(canvas, FONT_SIZE_MEDIUM);
     canvas_set_font_custom(canvas, FONT_SIZE_SMALL);
-    canvas_draw_str(canvas, 0, 30, "- github.com/JBlanked/FlipWorld");
+    canvas_draw_str(canvas, 0, 20, "Dev: JBlanked, codeallnight");
+    canvas_draw_str(canvas, 0, 30, "GFX: the1anonlypr3");
+    canvas_draw_str(canvas, 0, 40, "github.com/jblanked/FlipWorld");
 
     canvas_draw_str_multi(canvas, 0, 55, "The first open world multiplayer\ngame on the Flipper Zero.");
 }
@@ -659,8 +660,36 @@ static bool fetch_world_list(FlipperHTTP *fhttp)
         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list.json");
 
     fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v2/list/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(fhttp, "https://www.flipsocial.net/api/world/v3/list/10/", "{\"Content-Type\":\"application/json\"}");
 }
+// we will load the palyer stats from the API and save them
+// in player_spawn game method, it will load the player stats that we saved
+static bool fetch_player_stats(FlipperHTTP *fhttp)
+{
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "fhttp is NULL");
+        easy_flipper_dialog("Error", "fhttp is NULL. Press BACK to return.");
+        return false;
+    }
+    char username[64];
+    if (!load_char("Flip-Social-Username", username, sizeof(username)))
+    {
+        FURI_LOG_E(TAG, "Failed to load Flip-Social-Username");
+        easy_flipper_dialog("Error", "Failed to load saved username. Go to settings to update.");
+        return false;
+    }
+    char url[128];
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/user/game-stats/%s/", username);
+    snprintf(
+        fhttp->file_path,
+        sizeof(fhttp->file_path),
+        STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json");
+
+    fhttp->save_received_data = true;
+    return flipper_http_get_request_with_headers(fhttp, url, "{\"Content-Type\":\"application/json\"}");
+}
+
 static bool start_game_thread(void *context)
 {
     FlipWorldApp *app = (FlipWorldApp *)context;
@@ -688,31 +717,8 @@ static bool start_game_thread(void *context)
     furi_thread_start(thread);
     thread_id = furi_thread_get_id(thread);
     game_thread_running = true;
-    // view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
-    // view_dispatcher_send_custom_event(app->view_dispatcher, FlipWorldCustomEventPlay);
     return true;
 }
-static bool flip_world_fetch_world_list(DataLoaderModel *model)
-{
-    return fetch_world_list(model->fhttp);
-}
-static char *flip_world_parse_world_list(DataLoaderModel *model)
-{
-    FlipWorldApp *app = (FlipWorldApp *)model->parser_context;
-
-    if (!start_game_thread(app))
-    {
-        FURI_LOG_E(TAG, "Failed to start game thread");
-        easy_flipper_dialog("Error", "Failed to start game thread. Press BACK to return.");
-        view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu); // just go back to the main menu for now
-        return "Failed to start game thread";
-    }
-    return "Game starting... please wait :D";
-}
-void flip_world_world_list_switch_to_view(FlipWorldApp *app)
-{
-    return flip_world_generic_switch_to_view(app, "Fetching World List..", flip_world_fetch_world_list, flip_world_parse_world_list, 1, callback_to_submenu, FlipWorldViewLoader);
-}
 // combine register, login, and world list fetch into one function to switch to the loader view
 static bool flip_world_fetch_game(DataLoaderModel *model)
 {
@@ -837,7 +843,7 @@ static bool flip_world_fetch_game(DataLoaderModel *model)
 
         model->fhttp->save_received_data = true;
         char url[128];
-        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v2/get/world/%s/", furi_string_get_cstr(first_world));
+        snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/get/world/%s/", furi_string_get_cstr(first_world));
         furi_string_free(world_list);
         furi_string_free(first_world);
         return flipper_http_get_request_with_headers(model->fhttp, url, "{\"Content-Type\":\"application/json\"}");
@@ -1009,7 +1015,7 @@ void callback_submenu_choices(void *context, uint32_t index)
     {
     case FlipWorldSubmenuIndexRun:
         free_all_views(app, true, true);
-        if (!is_enough_heap(60000))
+        if (!is_enough_heap(45000)) // lowered from 60k to 45k since we saved 15k bytes
         {
             easy_flipper_dialog("Error", "Not enough heap memory.\nPlease restart your Flipper.");
             return;
@@ -1033,6 +1039,11 @@ void callback_submenu_choices(void *context, uint32_t index)
                 return fhttp->state != ISSUE;
             }
 
+            bool fetch_player_stats_i()
+            {
+                return fetch_player_stats(fhttp);
+            }
+
             Loading *loading;
             int32_t loading_view_id = 987654321; // Random ID
 
@@ -1051,7 +1062,8 @@ void callback_submenu_choices(void *context, uint32_t index)
             view_dispatcher_switch_to_view(app->view_dispatcher, loading_view_id);
 
             // Make the request
-            if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i))
+            if (!flipper_http_process_response_async(fhttp, fetch_world_list_i, parse_world_list_i) ||
+                !flipper_http_process_response_async(fhttp, fetch_player_stats_i, set_player_context))
             {
                 FURI_LOG_E(HTTP_TAG, "Failed to make request");
                 view_dispatcher_switch_to_view(app->view_dispatcher, FlipWorldViewSubmenu);
@@ -1426,7 +1438,7 @@ static bool flip_world_fetch_worlds(DataLoaderModel *model)
         sizeof(model->fhttp->file_path),
         STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/worlds/world_list_full.json");
     model->fhttp->save_received_data = true;
-    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v2/get/10/", "{\"Content-Type\":\"application/json\"}");
+    return flipper_http_get_request_with_headers(model->fhttp, "https://www.flipsocial.net/api/world/v3/get/10/", "{\"Content-Type\":\"application/json\"}");
 }
 static char *flip_world_parse_worlds(DataLoaderModel *model)
 {
@@ -1954,7 +1966,10 @@ void flip_world_generic_switch_to_view(FlipWorldApp *app, char *title, DataLoade
             model->data_text = NULL;
             //
             model->parser_context = app;
-            model->fhttp = flipper_http_alloc();
+            if (!model->fhttp)
+            {
+                model->fhttp = flipper_http_alloc();
+            }
         },
         true);
 

+ 36 - 0
easy_flipper/easy_flipper.c

@@ -587,4 +587,40 @@ bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer
     }
     furi_string_set_str(*furi_string, buffer);
     return true;
+}
+
+bool easy_flipper_set_text_box(
+    TextBox **text_box,
+    int32_t view_id,
+    char *text,
+    bool start_at_end,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher)
+{
+    if (!text_box)
+    {
+        FURI_LOG_E(EASY_TAG, "Invalid arguments provided to set_text_box");
+        return false;
+    }
+    *text_box = text_box_alloc();
+    if (!*text_box)
+    {
+        FURI_LOG_E(EASY_TAG, "Failed to allocate TextBox");
+        return false;
+    }
+    if (text)
+    {
+        text_box_set_text(*text_box, text);
+    }
+    if (previous_callback)
+    {
+        view_set_previous_callback(text_box_get_view(*text_box), previous_callback);
+    }
+    text_box_set_font(*text_box, TextBoxFontText);
+    if (start_at_end)
+    {
+        text_box_set_focus(*text_box, TextBoxFocusEnd);
+    }
+    view_dispatcher_add_view(*view_dispatcher, view_id, text_box_get_view(*text_box));
+    return true;
 }

+ 19 - 1
easy_flipper/easy_flipper.h

@@ -23,8 +23,8 @@
 #include <text_input/uart_text_input.h>
 #include <stdio.h>
 #include <string.h>
-#include <jsmn/jsmn.h>
 #include <jsmn/jsmn_furi.h>
+#include <jsmn/jsmn.h>
 
 #define EASY_TAG "EasyFlipper"
 
@@ -267,4 +267,22 @@ bool easy_flipper_set_loading(
  */
 bool easy_flipper_set_char_to_furi_string(FuriString **furi_string, char *buffer);
 
+/**
+ * @brief Initialize a TextBox object
+ * @param text_box The TextBox object to initialize
+ * @param view_id The ID/Index of the view
+ * @param text The text to display in the text box
+ * @param start_at_end Start the text box at the end
+ * @param previous_callback The previous callback function (can be set to NULL)
+ * @param view_dispatcher The ViewDispatcher object
+ * @return true if successful, false otherwise
+ */
+bool easy_flipper_set_text_box(
+    TextBox **text_box,
+    int32_t view_id,
+    char *text,
+    bool start_at_end,
+    uint32_t(previous_callback)(void *),
+    ViewDispatcher **view_dispatcher);
+
 #endif

+ 1 - 9
flip_world.c

@@ -6,12 +6,4 @@ char *yes_or_no_choices[] = {"No", "Yes"};
 int game_screen_always_on_index = 1;
 int game_sound_on_index = 0;
 int game_vibration_on_index = 0;
-bool is_enough_heap(size_t heap_size)
-{
-    size_t free_heap = memmgr_get_free_heap();
-
-    FURI_LOG_I(TAG, "Free heap: %d", free_heap);
-    FURI_LOG_I(TAG, "Total heap: %d", memmgr_get_total_heap());
-
-    return free_heap > (heap_size + 1024); // 1KB buffer
-}
+bool is_enough_heap(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); } // 1KB buffer

+ 8 - 3
flip_world.h

@@ -3,9 +3,14 @@
 #include <flipper_http/flipper_http.h>
 #include <easy_flipper/easy_flipper.h>
 
+// added by Derek Jamison to lower memory usage
+#undef FURI_LOG_E
+#define FURI_LOG_E(tag, msg, ...)
+//
+
 #define TAG "FlipWorld"
-#define VERSION 0.21
-#define VERSION_TAG "FlipWorld v0.2.1"
+#define VERSION 0.3
+#define VERSION_TAG "FlipWorld v0.3"
 
 // Define the submenu items for our FlipWorld application
 typedef enum
@@ -74,4 +79,4 @@ extern char *yes_or_no_choices[];
 extern int game_screen_always_on_index;
 extern int game_sound_on_index;
 extern int game_vibration_on_index;
-bool is_enough_heap(size_t heap_size);
+bool is_enough_heap(size_t heap_size);

+ 99 - 4
flipper_http/flipper_http.c

@@ -145,6 +145,101 @@ FuriString *flipper_http_load_from_file(char *file_path)
     return str_result;
 }
 
+FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit)
+{
+    // Open the storage record
+    Storage *storage = furi_record_open(RECORD_STORAGE);
+    if (!storage)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to open storage record");
+        return NULL;
+    }
+
+    // Allocate a file handle
+    File *file = storage_file_alloc(storage);
+    if (!file)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate storage file");
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // 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);
+        FURI_LOG_E(HTTP_TAG, "Failed to open file for reading: %s", file_path);
+        return NULL;
+    }
+
+    if (memmgr_get_free_heap() < limit)
+    {
+        FURI_LOG_E(HTTP_TAG, "Not enough heap to read file.");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Allocate a buffer to hold the read data
+    uint8_t *buffer = (uint8_t *)malloc(limit);
+    if (!buffer)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate buffer");
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Allocate a FuriString with preallocated capacity
+    FuriString *str_result = furi_string_alloc();
+    if (!str_result)
+    {
+        FURI_LOG_E(HTTP_TAG, "Failed to allocate FuriString");
+        free(buffer);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+    furi_string_reserve(str_result, limit);
+
+    // Read data into the buffer
+    size_t read_count = storage_file_read(file, buffer, limit);
+    if (storage_file_get_error(file) != FSE_OK)
+    {
+        FURI_LOG_E(HTTP_TAG, "Error reading from file.");
+        furi_string_free(str_result);
+        free(buffer);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+    if (read_count == 0)
+    {
+        FURI_LOG_E(HTTP_TAG, "No data read from file.");
+        furi_string_free(str_result);
+        free(buffer);
+        storage_file_close(file);
+        storage_file_free(file);
+        furi_record_close(RECORD_STORAGE);
+        return NULL;
+    }
+
+    // Append the entire buffer to FuriString in one operation
+    furi_string_cat_str(str_result, (char *)buffer);
+
+    // Clean up
+    storage_file_close(file);
+    storage_file_free(file);
+    furi_record_close(RECORD_STORAGE);
+    free(buffer);
+    return str_result;
+}
+
 // UART worker thread
 /**
  * @brief      Worker thread to handle UART data asynchronously.
@@ -507,14 +602,14 @@ bool flipper_http_send_data(FlipperHTTP *fhttp, const char *data)
 
     // Create a buffer with data + '\n'
     size_t send_length = data_length + 1; // +1 for '\n'
-    if (send_length > 256)
+    if (send_length > 512)
     { // Ensure buffer size is sufficient
         FURI_LOG_E("FlipperHTTP", "Data too long to send over FHTTP->");
         return false;
     }
 
-    char send_buffer[257]; // 256 + 1 for safety
-    strncpy(send_buffer, data, 256);
+    char send_buffer[513]; // 512 + 1 for safety
+    strncpy(send_buffer, data, 512);
     send_buffer[data_length] = '\n';     // Append newline
     send_buffer[data_length + 1] = '\0'; // Null-terminate
 
@@ -1047,7 +1142,7 @@ bool flipper_http_post_request_with_headers(
     }
 
     // Prepare POST request command with headers and data
-    char command[256];
+    char command[512];
     int ret = snprintf(
         command,
         sizeof(command),

+ 1 - 0
flipper_http/flipper_http.h

@@ -101,6 +101,7 @@ bool flipper_http_append_to_file(
     char *file_path);
 
 FuriString *flipper_http_load_from_file(char *file_path);
+FuriString *flipper_http_load_from_file_with_limit(char *file_path, size_t limit);
 
 // UART worker thread
 /**

+ 1 - 234
font/font.c

@@ -1,9 +1,6 @@
 #include <font/font.h>
 
 static const uint8_t u8g2_font_4x6_tf[];
-static const uint8_t u8g2_font_6x10_tf[];
-static const uint8_t u8g2_font_5x8_tf[];
-static const uint8_t u8g2_font_9x15_tf[];
 
 bool canvas_set_font_custom(Canvas *canvas, FontSize font_size)
 {
@@ -16,15 +13,6 @@ bool canvas_set_font_custom(Canvas *canvas, FontSize font_size)
     case FONT_SIZE_SMALL:
         canvas_set_custom_u8g2_font(canvas, u8g2_font_4x6_tf);
         break;
-    case FONT_SIZE_MEDIUM:
-        canvas_set_custom_u8g2_font(canvas, u8g2_font_5x8_tf);
-        break;
-    case FONT_SIZE_LARGE:
-        canvas_set_custom_u8g2_font(canvas, u8g2_font_6x10_tf);
-        break;
-    case FONT_SIZE_XLARGE:
-        canvas_set_custom_u8g2_font(canvas, u8g2_font_9x15_tf);
-        break;
     default:
         return false;
     }
@@ -92,225 +80,4 @@ static const uint8_t u8g2_font_4x6_tf[] =
     "\230Z\0\364\10\253\310e\230Z\0\365\7\253\310l\324\5\366\10\253\310\244\214\272\0\367\10\253\310e\264"
     "Q\2\370\7\243\310-\265\0\371\10\253\310\344\224T\22\372\10\253\310\246J%\1\373\10\253\310e\224T"
     "\22\374\10\253\310\244\234T\22\375\10\263\307\246j\304\5\376\11\263\307\304\250\322\212\0\377\11\263\307\244\234"
-    "F\134\0\0\0\0\4\377\377\0";
-/*
-  Fontname: -Misc-Fixed-Medium-R-Normal--8-80-75-75-C-50-ISO10646-1
-  Copyright: Public domain font.  Share and enjoy.
-  Glyphs: 191/1426
-  BBX Build Mode: 0
-*/
-static const uint8_t u8g2_font_5x8_tf[] =
-    "\277\0\2\2\3\4\3\4\4\5\10\0\377\6\377\6\0\1\32\2\61\6\226 \5\0~\3!\7\61c"
-    "\63R\0\42\7\233n\223\254\0#\15=bW\246\64T\65T\231\22\0$\12=b\233W\275S\332"
-    "\21%\10\253f\23Sg\0&\12<b\27S\263j\246\0'\5\31o\63(\7\262b\247\232\1)"
-    "\10\262b\23S\245\0*\12,b\23\223\32I\305\0+\12-b\233Q\34\62\243\10,\7\233^\247"
-    "J\0-\6\14j\63\2.\7\233^\227V\2/\10\64b_\266\63\0\60\10\263bW\271*\0\61"
-    "\7\263b\227dk\62\12\64b\247bN*\217\0\63\12\64b\63b\324H&\5\64\12\64b\33U"
-    "\65bN\0\65\12\64b\63\364F\62)\0\66\12\64b\247\362\212\62)\0\67\12\64b\63r\314\61"
-    "G\0\70\12\64b\247bRQ&\5\71\12\64b\247\242L;)\0:\7\252b\63\342\10;\10\263"
-    "^g#U\2<\7\263b\233\312\134=\10\34f\63\62\32\1>\10\263b\223\313T\2\77\11\263b"
-    "\327L\31&\0@\14E^+\243\134I%YC\5A\11\64b\247\242\34S\6B\12\64b\263\342"
-    "HQ\216\4C\11\64b\247\242.\223\2D\11\64b\263\242s$\0E\11\64b\63\364\312y\4F"
-    "\11\64b\63\364\312\65\0G\12\64b\247\242N\63)\0H\11\64b\23\345\230f\0I\7\263b\263"
-    "bkJ\11\64b\67sUF\0K\11\64b\23U\222\251\63L\10\64b\223\273G\0M\11\64b"
-    "\23\307\21\315\0N\11\64b\23\327Xg\0O\11\64b\247\242\63)\0P\12\64b\263\242\34)g"
-    "\0Q\11<^\247\242\134n\24R\12\64b\263\242\34)\312\0S\12\64b\247b\312\250L\12T\10"
-    "\263b\263b\27\0U\10\64b\23=\223\2V\11\64b\23\235I*\0W\11\64b\23\315q\304\0"
-    "X\12\64b\23e\222*\312\0Y\13\65b\223u\252\63\312(\2Z\11\64b\63rl\217\0[\7"
-    "\263b\63bs\134\12\64b\223\63\312(\243\34]\7\263b\63\233#^\6\223r\327\0_\6\14^"
-    "\63\2`\6\222r\23\3a\10$b\67\242L\3b\12\64b\223\363\212r$\0c\7\243b\67\263"
-    "\0d\11\64b_\215(\323\0e\10$b\247\322\310\12f\11\64b[\225\63G\0g\11,^\247"
-    "b\332I\1h\11\64b\223\363\212f\0i\10\263b\227\221\254\6j\11\273^\233a\251*\0k\11"
-    "\64b\223\313\221\242\14l\7\263b#\273\6m\11%b\243Z*\251\2n\7$b\263\242\31o\10"
-    "$b\247\242L\12p\11,^\263\342H\71\3q\10,^\67b\332\5r\10$b\223\222\235\1s"
-    "\7\243b\67\362\2t\12\64b\227\343\314)&\0u\7$b\23\315\64v\7\243b\223\254\12w\11"
-    "%b\223UR]\0x\10$b\23\223T\61y\12,^\23e\32\61)\0z\10$b\63b\71"
-    "\2{\13<b\253\62J\32\305\214\4|\5\61cs}\14<b\243Q\314He\224$\0~\7\24"
-    "r\227T\2\240\5\0~\3\241\7\61c\223F\0\242\11\64^\33Gj\316\4\243\12\64b[\215\230"
-    "\223J\0\244\12-b\223\323Lq\345\0\245\13\65b\223S\65d\34\62\2\246\6\71c\263\6\247\12"
-    "<b\67\362\212i\217\4\250\6\213v\223\2\251\12\65b\267\252\71U\265\0\252\7\253j\267\222\36\253"
-    "\10\34f\227TL\1\254\6\233b\63\13\255\5\213j\63\256\11\65b\367\241\226Z\0\257\5\213v\63"
-    "\260\6\233n\327\5\261\10\253b\227VF\3\262\7\253j\327Li\263\7\253j\243/\0\264\6\222r"
-    "\247\0\265\11,^\23\315\221\62\0\266\14\65b\67F\32)\251\230b\12\267\5\11k\23\270\6\222^"
-    "\247\0\271\7\253j\227d\65\272\7\253j\327\215\6\273\10\34f\223bR\11\274\12<b\223[Q\215"
-    "\230\0\275\12<b\223\253\244r\214\3\276\14<b\223Q\314HU#&\0\277\11\263b\227a\212\251"
-    "\2\300\12<b\227QTqL\31\301\11<b[\253\70\246\14\302\12<b\247bRqL\31\303\12"
-    "<b\227TTqL\31\304\12<b\23\63TqL\31\305\12<b\247bRqL\31\306\11\64b"
-    "\67Rk\250J\307\12<^\247\242.\223\214\0\310\12<b\227Q\32z\345\21\311\11<b[\16\275"
-    "\362\10\312\12<b\247\342\330+\217\0\313\12<b\23\63\32z\345\21\314\11\273b\223\323\212\325\0\315"
-    "\11\273b\233\322\212\325\0\316\11\273bW\215\24\253\1\317\11\273b\223\362\212\325\0\320\13\65b\67\343"
-    "He\212i\1\321\12<b\227T\271\324\224\1\322\12<b\227QT\321L\12\323\11<b[\253h"
-    "&\5\324\12<b\247bR\321L\12\325\12<b\227TT\321L\12\326\12<b\23\63T\321L\12"
-    "\327\6\233b\223:\330\11\64b\67\322\221\216\4\331\11<b\227Q\351L\12\332\10<b\333t&\5"
-    "\333\11<b\247\242gR\0\334\12<b\23\63\212\316\244\0\335\13=b_\346Tg\224Q\4\336\12"
-    "\64b\223W\224#e\0\337\11\64b\247\242\352T\11\340\12<b\227Q\306#\312\64\341\11<b["
-    "S#\312\64\342\12<b[e\70\242L\3\343\12<b\227T\306#\312\64\344\11\64bW\303\21e"
-    "\32\345\12<b\247b\222#\312\64\346\11%b\63\242\62G\0\347\10\253^\67\263J\0\350\13<b"
-    "\227Q\306*\215\254\0\351\12<b[S*\215\254\0\352\13<b\247b\206*\215\254\0\353\12\64b"
-    "WC\225FV\0\354\11\273b\223\63\222\325\0\355\11\273b\233\62\222\325\0\356\10\273b\327\226\325\0"
-    "\357\10\263b\223\262\254\6\360\14<b\223b\225Q\32\61)\0\361\12<b\227T\206+\232\1\362\13"
-    "<b\227Q\306*\312\244\0\363\12<b[S*\312\244\0\364\13<b\247b\206*\312\244\0\365\13"
-    "<b\227T\306*\312\244\0\366\12\64b\23\63TQ&\5\367\10\253b\227\321F\11\370\11$b\67"
-    "\322H#\1\371\12<b\227Q\206\321L\3\372\11<b[\343h\246\1\373\12<b\247bF\321L"
-    "\3\374\11\64b\23\63\212f\32\375\13D^[\343(\323\210I\1\376\12<^\223\363\212#\345\14\377"
-    "\14<^\23\63\212\62\215\230\24\0\0\0\0\4\377\377\0";
-/*
-  Fontname: -Misc-Fixed-Medium-R-Normal--10-100-75-75-C-60-ISO10646-1
-  Copyright: Public domain terminal emulator font.  Share and enjoy.
-  Glyphs: 191/1597
-  BBX Build Mode: 0
-*/
-static const uint8_t u8g2_font_6x10_tf[] =
-    "\277\0\2\2\3\4\3\5\4\6\12\0\376\7\376\7\0\1B\2\222\7\263 \5\0b\7!\7\71C"
-    "g\250\0\42\7\233R'Y\1#\15=B\257Li\250j\250\62%\0$\13=B\67\257z\247\264"
-    "#\0%\13=B/\252\356\252%\23\0&\14=B/\247\230r\225dT\1'\5\31Sg(\10"
-    "\273B\67\225u\1)\10\273B'\227U\11*\12-F'\247j\250v\0+\12-F\67\243\70d"
-    "F\21,\7\233>O\225\0-\6\15Ng\10.\7\233>/\255\4/\13=B\37e\224\273QF"
-    "\0\60\12=B\67\247\332Nu\4\61\14=B\67\313\224QF\31\305!\62\14=Bo\345\214\242\314"
-    "\31\15\1\63\14=Bgh\224\263\206:-\0\64\14=B\77\313T\246\241\63J\0\65\13=B\347"
-    "F\311\314H\247\5\66\13=BW\346\214\222\251\323\2\67\14=Bgh\224\63\312\65\312\0\70\13="
-    "Boe\235V\326i\1\71\14=Boe\251TF\71J\0:\12\273>/\255\14\323J\0;\11"
-    "\273>/\255\14U\11<\12\274B\77\266QF\31\5=\10\35Jgh\70\4>\13\274B'\243\214"
-    "\62\212m\0\77\12=Bo\345\66\312t\4@\13=Boe\271\222\225\341\2A\13=B\67\247Z"
-    "\217\221u\0B\14=Bg\304*\246Y\305\241\0C\14=Boe\215\62\312(\247\5D\15=B"
-    "g\304*\246\230b\212C\1E\14=B\347F\31\215\224QFCF\15=B\347F\31\215\224QF"
-    "\31\1G\14=Boe\215\62\212;-\0H\11=B'\333cd;I\10\273Bg\305\256\1J"
-    "\14=Bwg\224QFe\224\0K\13=B'\313T\352\24\253\34L\16=B'\243\214\62\312("
-    "\243\214\206\0M\12=B'\353\265\222\266\3N\12=B'\353\251\222\334:O\11=Boe\357\264"
-    "\0P\15=Bg\244\254\207\312(\243\214\0Q\12E>oe\257j\303\0R\13=Bg\244\254\207"
-    "*\253\34S\13=Boe\15\67\324i\1T\16=Bg\310\214\62\312(\243\214\42\0U\10=B"
-    "'\373N\13V\13=B'\333\251L\61\345\10W\12=B'\273\222Jw\0X\12=B'\353T"
-    "W\265\16Y\14=B'\353Tg\224QF\21Z\12=Bgh\224\273\321\20[\10\273Bg\304\316"
-    "\1\134\15=B'\243\14\63\314\60\303\214\2]\10\273Bgv\216\0^\7\35R\67\247:_\6\15"
-    ">g\10`\6\22['\6a\12-Bo\303\64t\32\1b\14=B'\243\214\222\251\247R\0c"
-    "\12-Boe\215rZ\0d\13=B\37e\224\314-\225\12e\12-Bo\345\61\62\134\0f\14"
-    "=BWVy\304\214\62\312\0g\14=:oh\235FF:-\0h\13=B'\243\214\222\251\355"
-    "\0i\10\273B/#\331\32j\13\314:\77c]K\231\24\0k\14=B'\243\214\262L\263\312\1"
-    "l\7\273BG\366\32m\12-BG\265TRI\7n\10-B'\231\332\16o\11-Boe;"
-    "-\0p\14=:'\231z*\225QF\0q\13=:\317\334R\251\214\62\12r\13-B'\231\32"
-    "e\224\21\0s\12-Boe\270\341P\0t\15=B/\243<bF\31\305\250\0u\10-B'"
-    ";\225\12v\12-B'\353T\246\34\1w\11-B'[Iu\1x\11-B'\247\272\252\3y"
-    "\13=:'\233Je\244\323\2z\10-Bg\350\366\20{\13\274BWe\224\64\212\31\11|\6\71"
-    "C\347\10}\14\274BG\243\230\221\312(I\0~\11\35R/\252$\23\0\240\5\0b\7\241\7\71"
-    "C'\15\1\242\14=>\67\17\25SLy\304\10\243\13=BWVyg\24\225\2\244\12-B'"
-    "\247\231\342\312\1\245\15E>'\353T\307!\63\312(\2\246\6\71Cg\15\247\13E>oe\64;"
-    "\67J\13\250\6\213^'\5\251\14=Boe\225\246J:-\0\252\12\264FoD\245j\64\2\253"
-    "\13.B\267\212)\346\230c\0\254\7\224Jg\344\0\255\6\214Ng\4\256\13=Bo\345\221\346\324"
-    "i\1\257\6\15^g\10\260\6\233R\257\13\261\13\65B\67\243\70dFq\10\262\10\254NO\305\346"
-    "\10\263\12\254Ng\243\244\321H\0\264\6\22[O\1\265\12\65>'\333S\251\214\0\266\16=Bo"
-    "\214\64RR\61\305\24S\0\267\5\11O'\270\6\22;O\1\271\7\253N/\311j\272\12\264FO"
-    "E\231\64\34\1\273\14.B'\346\230c\212)F\0\274\20N>/#\15\63\314hf\244S\34\31"
-    "\6\275\20N>/#\15\63\314heT\303\214\62\32\276\16M>G\303\234a\224Y\246\64\62\12\277"
-    "\12=B\67\323\31\345\326\2\300\14EB/\303\274\262\36#\353\0\301\13EB\277^Y\217\221u\0"
-    "\302\14EB\67\247\270\262\36#\353\0\303\14EB/*\271\262\36#\353\0\304\13EB\257\246V\326"
-    "cd\35\305\14EB\67\247\270\262\36#\353\0\306\13>Bw\244\262\71Fl\16\307\15M:oe"
-    "\215\62\312(\247]\3\310\16EB/\217\215\62\32)\243\214\206\0\311\16EB\77\215\215\62\32)\243"
-    "\214\206\0\312\16EB\67\216\215\62\32)\243\214\206\0\313\16EB\257\32\33e\64RF\31\15\1\314"
-    "\11\303B'\247\25[\3\315\11\303B\67\245\25[\3\316\11\303B\257\32)\266\6\317\11\303B'\345"
-    "\25[\3\320\15=Bg\304*\216T\246\70\24\0\321\13EB\67uO\225\344\326\1\322\13EB/"
-    "\303\274\262;-\0\323\12EB\277^\331\235\26\0\324\13EB\67\247\270\262;-\0\325\12EB\67"
-    "\65Wv\247\5\326\12EB\257\246Vv\247\5\327\11-B'\247\272\252\3\330\13=Bo\305+\315"
-    "\231\26\0\331\12EB/\303\332;-\0\332\11EB\277\314\336i\1\333\13EB\67\247\214\263;-"
-    "\0\334\12EB\257\306\331;-\0\335\14EB\277\314:\325\31e\24\1\336\16=B'\243\221\362P"
-    "\31e\224\21\0\337\13=Boe\231\312*+\5\340\14EB/\303Ln\230\206N#\341\13EB"
-    "\277&\67LC\247\21\342\14EB\67\247Lm\230\206N#\343\14EB\67\265\251\15\323\320i\4\344"
-    "\13=B\257\246\66LC\247\21\345\14EB\67\247\234\67LC\247\21\346\13.BodT\215\231\207"
-    "\0\347\13=:oe\215r\332\65\0\350\14EB/\303L\256<F\206\13\351\13EB\277&W\36"
-    "#\303\5\352\14EB\67\247L\255<F\206\13\353\13=B\257\246V\36#\303\5\354\11\303B'g"
-    "$[\3\355\10\303B\257\206\262\65\356\10\303B\257-[\3\357\10\273B'e\331\32\360\13=BG"
-    "C\271\262\235\26\0\361\12EB\67\265q\62\265\35\362\13EB/\303L\256l\247\5\363\12EB\277"
-    "&W\266\323\2\364\13EB\67\247L\255l\247\5\365\13EB\67\265\251\225\355\264\0\366\12=B\257"
-    "\246V\266\323\2\367\11-F\67SCS\21\370\12-Bo\310\225\346P\0\371\13EB/\303Le"
-    "\247R\1\372\12EB\277\246\262S\251\0\373\13EB\67\247\214\263S\251\0\374\12=B\257\306\331\251"
-    "T\0\375\14M:\277\314\246R\31\351\264\0\376\15E:'\243\221\262=TF\31\1\377\15M:\257"
-    "\306\331T*#\235\26\0\0\0\0\4\377\377\0";
-
-/*
-  Fontname: -Misc-Fixed-Medium-R-Normal--15-140-75-75-C-90-ISO10646-1
-  Copyright: Public domain font.  Share and enjoy.
-  Glyphs: 191/4777
-  BBX Build Mode: 0
-*/
-static const uint8_t u8g2_font_9x15_tf[] =
-    "\277\0\3\2\4\4\4\5\5\11\17\0\375\12\375\13\377\1\223\3*\12\21 \5\0\310\63!\10\261\14"
-    "\63\16\221\0\42\10\64{\63\42S\0#\16\206\31s\242\226a\211Z\206%j\1$\24\267\371\362\302"
-    "A\211\42)L\322\65\11#\251\62\210\31\0%\21\247\11sB%JJ\255q\32\265\224\22\61\1&"
-    "\22\247\11s\304(\213\262(T\65)\251E\225H\13'\6\61|\63\6(\14\303\373\262\222(\211z"
-    "\213\262\0)\14\303\373\62\262(\213z\211\222\10*\15w\71\363J\225\266-i\252e\0+\13w\31"
-    "\363\342\332\60dq\15,\11R\334\62\206$Q\0-\7\27I\63\16\1.\7\42\14\63\206\0/\14"
-    "\247\11\263\323\70-\247\345\64\6\60\15\247\11\263\266J\352k\222e\23\0\61\15\247\11\363R\61\311\242"
-    "\270\267a\10\62\14\247\11s\6%U\373y\30\2\63\16\247\11\63\16qZ\335\343XM\6\5\64\21"
-    "\247\11sS\61\311\242Z\22&\303\220\306\25\0\65\17\247\11\63\16reH\304\270\254&\203\2\66\20"
-    "\247\11\263\206(\215+C\42\252\326dP\0\67\16\247\11\63\16q\32\247q\32\247q\10\70\21\247\11"
-    "\263\266J\232d\331VI\325$\313&\0\71\17\247\11s\6%uT\206$\256FC\4:\10r\14"
-    "\63\206x\10;\12\242\334\62\206xH\22\5<\11\245\12\63\263\216i\7=\12G)\63\16\71>\14"
-    "\1>\12\245\12\63\322\216YG\0\77\16\247\11s\6%U\343\264\71\207\63\0@\21\247\11s\6%"
-    "\65\15J\246D\223\42\347\203\2A\15\247\11\363\322$\253\244\326\341j\15B\22\247\11\63\6)LR"
-    "\61\31\244\60I\215\311 \1C\15\247\11s\6%\225\373\232\14\12\0D\15\247\11\63\6)LR\77"
-    "&\203\4E\15\247\11\63\16ry\220\342\346a\10F\14\247\11\63\16ry\220\342\316\0G\17\247\11"
-    "s\6%\225\333\206\324\232\14\12\0H\12\247\11\63R\327\341\352\65I\12\245\12\63\6)\354\247AJ"
-    "\15\250\11\363\6\65\357S\230\15\31\0K\21\247\11\63R\61\311\242\332\230\204QV\12\223\64L\12\247"
-    "\11\63\342\376<\14\1M\20\247\11\63Ru[*JE\212\244H\265\6N\17\247\11\63RuT\62"
-    ")\322\22q\265\6O\14\247\11s\6%\365\327dP\0P\15\247\11\63.\251u\30\222\270\63\0Q"
-    "\16\307\351r\6%\365K&U\6\65\27R\20\247\11\63.\251u\30\222(+\205I\252\6S\16\247"
-    "\11s\6%\265\357V\65\31\24\0T\12\247\11\63\16Y\334\337\0U\13\247\11\63R\377\232\14\12\0"
-    "V\21\247\11\63Rk\222EY\224U\302$L\322\14W\20\247\11\63RO\221\24I\221\24)\335\22"
-    "\0X\20\247\11\63R\65\311*i\234&Y%U\3Y\15\247\11\63R\65\311*i\334\33\0Z\14"
-    "\247\11\63\16q\332\347x\30\2[\12\304\373\62\6\255\177\33\2\134\13\247\11\63\362\70/\347\345<]"
-    "\12\304\372\62\206\254\177\33\4^\12Gi\363\322$\253\244\1_\7\30\370\62\16\2`\7\63\213\63\262"
-    "\2a\16w\11s\6=N\206!\25\225!\11b\17\247\11\63\342\226!\21U\353\250\14\11\0c\14"
-    "w\11s\6%\225[\223A\1d\15\247\11\263[\206D\134\35\225!\11e\15w\11s\6%U\207"
-    "s>(\0f\16\247\11\363\266R\26\305\341 \306\215\0g\23\247\331r\206DL\302$\214\206(\37"
-    "\224TM\6\5h\14\247\11\63\342\226!\21U\257\1i\12\245\12stt\354i\20j\15\326\331\62"
-    "u\312\332U\64&C\2k\17\247\11\63\342VMI\64\65\321\62%\15l\11\245\12\63\306\376\64\10"
-    "m\20w\11\63\26%\212\244H\212\244H\212\324\0n\13w\11\63\222!\21U\257\1o\14w\11s"
-    "\6%\365\232\14\12\0p\17\247\331\62\222!\21U\353\250\14I\134\6q\15\247\331r\206D\134\35\225"
-    "!\211\33r\14w\11\63\242IK\302$n\5s\15w\11s\6%\325\7]M\6\5t\14\227\11"
-    "\263\342p\330\342n\331\2u\20w\11\63\302$L\302$L\302$\214\206$v\16w\11\63R\65\311"
-    "\242\254\22&i\6w\16w\11\63RS$ER\244tK\0x\15w\11\63\322$\253\244\225\254\222"
-    "\6y\15\246\331\62B\337\224%\25\223!\1z\12w\11\63\16i\257\303\20{\15\305\373\262\226\260\32"
-    "ij\26V\7|\6\301\374\62>}\16\305\371\62\326\260\226jR\32V&\0~\12\67ys\64)"
-    "\322\24\0\240\5\0\310\63\241\10\261\14\63\244a\10\242\21\206\11\63\243!\211\22\251\222%Q\62D!"
-    "\0\243\21\247\11\363\266R\34\16b\234\212I\226$\13\0\244\16g\71\63\322d\220\262(\213\6%\15"
-    "\245\20\247\11\63R\65\311*\331 \206\203\30\327\0\246\10\261\374\62\6e\20\247\16\264\372r\224HT"
-    "\42S\42J\211\2\250\7%\232\63\62-\251\25\230\30\263\206,L\42K\224(\241\22%\222\224\204\331"
-    "\20\1\252\14u\71s\244\322\22EC:\10\253\15\207\31\363\242~\213\302(\214\302(\254\7F)\63"
-    "\256\15\255\7\25J\63\6\1\256\25\230\30\263\206,L\222I\211\22eRJJ\224\24\263!\2\257\6"
-    "\26\231\63\16\260\12Dks\224HJ\24\0\261\16\227\31\363\342\332\60dq\35\33\206\0\262\13dI"
-    "s\224(K\224l\10\263\13dIs\224\250(%\12\0\264\10\63\213\263\222\22\0\265\14\227\351\62R"
-    "\257\333\262\310\61\0\266\26\247\11s\206!K\264DK\222!\11\223\60\11\223\60\11\223\0\267\7\42L"
-    "\63\206\0\270\10\64\332\262\246D\1\271\10cIs\22\251e\272\12eIs\226LK\346A\273\16\207"
-    "\31\63\242\60\12\243\60\312\242~\3\274\17\247\11sR\271q\210\304$\213\62%\25\275\16\247\11sR"
-    "\271I\31\242\70\24\343!\276\21\247\11s\304(\315\263\250\42\211I\26eJ*\277\15\247\11\363r\270"
-    "\332\234\252\311\240\0\300\16\307\11s\362:\272URu\270Z\3\301\15\307\11s\333\321\255\222\252\303\325"
-    "\32\302\17\307\11\363\322$\253c[%U\207\253\65\303\17\267\11s\64i\307\266J\252\16Wk\0\304"
-    "\17\267\11s\262(\313\261\255\222\252\303\325\32\305\17\267\11\263\266\332\230d\225T\35\256\326\0\306\25\247"
-    "\11s\224!\312\242,\312\242lX\242,\312\242,\32\2\307\20\327\331r\6%\225\373\232\14\242\26\205"
-    "\32\0\310\21\307\11s\362:\66\14I\34\17Y\134\35\206\0\311\20\307\11s\333\261aH\342x\310\342"
-    "\352\60\4\312\22\307\11\363\322$\253#\303\220\304\361\220\305\325a\10\313\22\267\11s\262(\313\221aH"
-    "\342x\310\342\352\60\4\314\14\305\12\63\322\372 \205=\15\2\315\14\305\12\63\263\372 \205=\15\2\316"
-    "\15\305\12\263\262\244\226\16R\330\323 \317\14\265\12\63\62-\35\244\260\247A\320\25\250\10s\6-\214"
-    "\322$\35\302$M\322$M\302h\220\0\321\22\267\11s\64iG\322Q\311\244H\212\264D\134\3\322"
-    "\16\307\11s\362:\70(\251\257\311\240\0\323\15\307\11s\333\301AI}M\6\5\324\20\307\11\363\322"
-    "$\253C\203\222\372\232\14\12\0\325\17\267\11s\64i\207\6%\365\65\31\24\0\326\17\267\11s\262("
-    "\313\241AI}M\6\5\327\15w\31\63\322$\253\244\225\254\222\6\330\26\307\371\262\223A\11\267DK"
-    "\244H\212\224L\311\306dPb\0\331\15\307\11s\362:\226\372\65\31\24\0\332\14\307\11s\333\261\324"
-    "\257\311\240\0\333\16\307\11\363\322$\253#\251_\223A\1\334\16\267\11s\262(\313\221\324\257\311\240\0"
-    "\335\16\307\11s\333\261TM\262J\32\267\1\336\16\247\11\63\342xXR\353\60$q\31\337\24\246\11"
-    "\263\246,\311\222(Q\262\250\226dI\226$\12\0\340\21\267\11\263\362:\66\350q\62\14\251\250\14I"
-    "\0\341\20\267\11s\333\301A\217\223aHEeH\2\342\22\267\11\363\322$\253C\203\36'\303\220\212"
-    "\312\220\4\343\22\247\11\263\244$\322\241A\217\223aHEeH\2\344\22\247\11s\262(\313\241A\217"
-    "\223aHEeH\2\345\22\267\11\363\304(\324\261A\217\223aHEeH\2\346\17w\11s,Q"
-    "-J\6%\312\242\212\62\347\17\247\331r\6%\225[\223A\324\242P\3\350\17\267\11s\362:\70("
-    "\251:\234\363A\1\351\17\267\11s\333\301AI\325\341\234\17\12\0\352\21\267\11\363\322$\253C\203\222"
-    "\252\303\71\37\24\0\353\21\247\11s\262(\313\241AI\325\341\234\17\12\0\354\12\265\12\63\322\372\330\323"
-    " \355\12\265\12\363\332\221\261\247A\356\15\266\11\263\302$\312rd\355m\20\357\14\245\12\63\242$\212"
-    "\307\236\6\1\360\20\267\11s\242PL\362lPR\257\311\240\0\361\16\247\11s\64iG\222!\21U"
-    "\257\1\362\16\267\11s\362:\70(\251\327dP\0\363\15\267\11s\333\301AI\275&\203\2\364\17\267"
-    "\11\363\322$\253C\203\222zM\6\5\365\17\247\11s\64i\207\6%\365\232\14\12\0\366\17\247\11s"
-    "\262(\313\241AI\275&\203\2\367\16\227\11\363\322\65\307\206!\307\322\65\3\370\23\227\371\262\223A\311"
-    "\22-\221\42%S\262dPb\0\371\23\267\11s\362:\26&a\22&a\22&a\64$\1\372\22"
-    "\267\11s\333\261\60\11\223\60\11\223\60\11\243!\11\373\24\267\11\363\322$\253#a\22&a\22&a"
-    "\22FC\22\374\24\247\11s\242,\312\241\60\11\223\60\11\223\60\11\243!\11\375\17\346\331\62\333\241\320"
-    "\67eI\305dH\0\376\20\307\331\62\342\226!\21UuT\206$.\3\377\17\326\331r\242\366\320\67"
-    "eI\305dH\0\0\0\0\4\377\377\0";
+    "F\134\0\0\0\0\4\377\377\0";

+ 0 - 3
font/font.h

@@ -5,9 +5,6 @@
 typedef enum
 {
     FONT_SIZE_SMALL = 1,
-    FONT_SIZE_MEDIUM = 2,
-    FONT_SIZE_LARGE = 3,
-    FONT_SIZE_XLARGE = 4
 } FontSize;
 extern bool canvas_set_font_custom(Canvas *canvas, FontSize font_size);
 extern void canvas_draw_str_multi(Canvas *canvas, uint8_t x, uint8_t y, const char *str);

+ 24 - 6
game/game.c

@@ -15,13 +15,31 @@ static void game_start(GameManager *game_manager, void *ctx)
     game_context->player_context = NULL;
     game_context->current_level = 0;
     game_context->ended_early = false;
-    if (!allocate_level(game_manager, 0))
+    game_context->level_count = 0;
+
+    // set all levels to NULL
+    for (int i = 0; i < MAX_LEVELS; i++)
     {
-        FURI_LOG_E("Game", "Failed to allocate level 0");
-        return;
+        game_context->levels[i] = NULL;
+    }
+
+    // attempt to allocate all levels
+    for (int i = 0; i < MAX_LEVELS; i++)
+    {
+        if (!allocate_level(game_manager, i))
+        {
+            if (i == 0)
+            {
+                FURI_LOG_E("Game", "Failed to allocate level %d, loading default level", i);
+                game_context->levels[0] = game_manager_add_level(game_manager, generic_level("town_world_v2", 0));
+                game_context->level_count = 1;
+                break;
+            }
+            FURI_LOG_E("Game", "No more levels to load");
+            break;
+        }
+        game_context->level_count++;
     }
-    game_context->level_count = 1;
-    game_context->levels[1] = NULL;
 
     // imu
     game_context->imu = imu_alloc();
@@ -63,7 +81,7 @@ static void game_stop(void *ctx)
             easy_flipper_dialog("Game Over", "Ran out of memory so the\ngame ended early.\nHit BACK to exit.");
         }
         FURI_LOG_I("Game", "Saving player context");
-        save_player_context(game_context->player_context);
+        save_player_context_api(game_context->player_context);
         FURI_LOG_I("Game", "Player context saved");
         easy_flipper_dialog("Game Saved", "Hit BACK to exit.");
     }

+ 24 - 24
game/icon.c

@@ -244,18 +244,18 @@ IconContext *get_icon_context(const char *name)
     {
         return icon_generic_alloc("lake_top_right", &I_icon_lake_top_right_24x22px, 24, 22);
     }
-    // else if (strcmp(name, "rock_large") == 0)
-    // {
-    //     return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
-    // }
-    // else if (strcmp(name, "rock_medium") == 0)
-    // {
-    //     return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
-    // }
-    // else if (strcmp(name, "rock_small") == 0)
-    // {
-    //     return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8);
-    // }
+    else if (strcmp(name, "rock_large") == 0)
+    {
+        return icon_generic_alloc("rock_large", &I_icon_rock_large_18x19px, 18, 19);
+    }
+    else if (strcmp(name, "rock_medium") == 0)
+    {
+        return icon_generic_alloc("rock_medium", &I_icon_rock_medium_16x14px, 16, 14);
+    }
+    else if (strcmp(name, "rock_small") == 0)
+    {
+        return icon_generic_alloc("rock_small", &I_icon_rock_small_10x8px, 10, 8);
+    }
 
     // If no match is found
     FURI_LOG_E("Game", "Icon not found: %s", name);
@@ -376,18 +376,18 @@ const char *icon_get_id(const Icon *icon)
     {
         return "lake_top_right";
     }
-    // else if (icon == &I_icon_rock_large_18x19px)
-    // {
-    //     return "rock_large";
-    // }
-    // else if (icon == &I_icon_rock_medium_16x14px)
-    // {
-    //     return "rock_medium";
-    // }
-    // else if (icon == &I_icon_rock_small_10x8px)
-    // {
-    //     return "rock_small";
-    // }
+    else if (icon == &I_icon_rock_large_18x19px)
+    {
+        return "rock_large";
+    }
+    else if (icon == &I_icon_rock_medium_16x14px)
+    {
+        return "rock_medium";
+    }
+    else if (icon == &I_icon_rock_small_10x8px)
+    {
+        return "rock_small";
+    }
 
     // If no match is found
     FURI_LOG_E("Game", "Icon ID not found for given icon pointer.");

+ 2 - 1
game/level.c

@@ -23,7 +23,8 @@ bool allocate_level(GameManager *manager, int index)
         furi_string_free(world_list);
         return false;
     }
-    game_context->levels[game_context->current_level] = game_manager_add_level(manager, generic_level(furi_string_get_cstr(world_name), index));
+    FURI_LOG_I("Game", "Allocating level %d for world %s", index, furi_string_get_cstr(world_name));
+    game_context->levels[index] = game_manager_add_level(manager, generic_level(furi_string_get_cstr(world_name), index));
     furi_string_free(world_name);
     furi_string_free(world_list);
     return true;

+ 12 - 9
game/player.c

@@ -9,16 +9,21 @@ static Level *get_next_level(GameManager *manager)
         FURI_LOG_E(TAG, "Failed to get game context");
         return NULL;
     }
-    game_context->current_level = game_context->current_level == 0 ? 1 : 0;
-    if (!game_context->levels[game_context->current_level])
+    for (int i = game_context->current_level + 1; i < game_context->level_count; i++)
     {
-        if (!allocate_level(manager, game_context->current_level))
+        if (!game_context->levels[i])
         {
-            FURI_LOG_E(TAG, "Failed to allocate level %d", game_context->current_level);
-            return NULL;
+            if (!allocate_level(manager, i))
+            {
+                FURI_LOG_E(TAG, "Failed to allocate level %d", i);
+                return NULL;
+            }
         }
+        game_context->current_level = i;
+        return game_context->levels[i];
     }
-    return game_context->levels[game_context->current_level];
+    FURI_LOG_I(TAG, "No more levels to load");
+    return NULL;
 }
 
 void player_spawn(Level *level, GameManager *manager)
@@ -262,7 +267,6 @@ static void player_update(Entity *self, GameManager *manager, void *context)
     // switch levels if holding OK
     if (input.pressed & GameKeyOk)
     {
-        FURI_LOG_I(TAG, "Player is pressing OK");
         // if all enemies are dead, allow the "OK" button to switch levels
         // otherwise the "OK" button will be used to attack
         if (game_context->enemy_count == 0)
@@ -271,14 +275,13 @@ static void player_update(Entity *self, GameManager *manager, void *context)
             save_player_context(player);
             game_manager_next_level_set(manager, get_next_level(manager));
             furi_delay_ms(500);
+            return;
         }
         else
         {
-            FURI_LOG_I(TAG, "Player is attacking");
             game_context->user_input = GameKeyOk;
             // furi_delay_ms(100);
         }
-        FURI_LOG_I(TAG, "Player is done pressing OK");
     }
 
     // If the player is not moving, retain the last movement direction

+ 340 - 0
game/storage.c

@@ -201,6 +201,197 @@ bool save_player_context(PlayerContext *player_context)
     return true;
 }
 
+bool save_player_context_api(PlayerContext *player_context)
+{
+    if (!player_context)
+    {
+        FURI_LOG_E(TAG, "Invalid player context");
+        return false;
+    }
+
+    FlipperHTTP *fhttp = flipper_http_alloc();
+    if (!fhttp)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate FlipperHTTP");
+        return false;
+    }
+
+    // create JSON for all the player context data
+    FuriString *json = furi_string_alloc();
+    if (!json)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate JSON string");
+        return false;
+    }
+
+    // opening brace
+    furi_string_cat_str(json, "{");
+
+    // 1. Username (String)
+    furi_string_cat_str(json, "\"username\":\"");
+    furi_string_cat_str(json, player_context->username);
+    furi_string_cat_str(json, "\",");
+
+    // 2. Level (uint32_t)
+    furi_string_cat_str(json, "\"level\":");
+    char buffer[32];
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->level);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 3. XP (uint32_t)
+    furi_string_cat_str(json, "\"xp\":");
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->xp);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 4. Health (uint32_t)
+    furi_string_cat_str(json, "\"health\":");
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->health);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 5. Strength (uint32_t)
+    furi_string_cat_str(json, "\"strength\":");
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->strength);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 6. Max Health (uint32_t)
+    furi_string_cat_str(json, "\"max_health\":");
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->max_health);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 7. Health Regen (uint32_t)
+    furi_string_cat_str(json, "\"health_regen\":");
+    snprintf(buffer, sizeof(buffer), "%lu", player_context->health_regen);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 8. Elapsed Health Regen (float)
+    furi_string_cat_str(json, "\"elapsed_health_regen\":");
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_health_regen);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 9. Attack Timer (float)
+    furi_string_cat_str(json, "\"attack_timer\":");
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->attack_timer);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 10. Elapsed Attack Timer (float)
+    furi_string_cat_str(json, "\"elapsed_attack_timer\":");
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->elapsed_attack_timer);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 11. Direction (enum PlayerDirection)
+    furi_string_cat_str(json, "\"direction\":");
+    switch (player_context->direction)
+    {
+    case PLAYER_UP:
+        furi_string_cat_str(json, "\"up\",");
+        break;
+    case PLAYER_DOWN:
+        furi_string_cat_str(json, "\"down\",");
+        break;
+    case PLAYER_LEFT:
+        furi_string_cat_str(json, "\"left\",");
+        break;
+    case PLAYER_RIGHT:
+    default:
+        furi_string_cat_str(json, "\"right\",");
+        break;
+    }
+
+    // 12. State (enum PlayerState)
+    furi_string_cat_str(json, "\"state\":");
+    switch (player_context->state)
+    {
+    case PLAYER_IDLE:
+        furi_string_cat_str(json, "\"idle\",");
+        break;
+    case PLAYER_MOVING:
+        furi_string_cat_str(json, "\"moving\",");
+        break;
+    case PLAYER_ATTACKING:
+        furi_string_cat_str(json, "\"attacking\",");
+        break;
+    case PLAYER_ATTACKED:
+        furi_string_cat_str(json, "\"attacked\",");
+        break;
+    case PLAYER_DEAD:
+        furi_string_cat_str(json, "\"dead\",");
+        break;
+    default:
+        furi_string_cat_str(json, "\"unknown\",");
+        break;
+    }
+
+    // 13. Start Position X (float)
+    furi_string_cat_str(json, "\"start_position_x\":");
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.x);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 14. Start Position Y (float)
+    furi_string_cat_str(json, "\"start_position_y\":");
+    snprintf(buffer, sizeof(buffer), "%.6f", (double)player_context->start_position.y);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 15. dx (int8_t)
+    furi_string_cat_str(json, "\"dx\":");
+    snprintf(buffer, sizeof(buffer), "%d", player_context->dx);
+    furi_string_cat_str(json, buffer);
+    furi_string_cat_str(json, ",");
+
+    // 16. dy (int8_t)
+    furi_string_cat_str(json, "\"dy\":");
+    snprintf(buffer, sizeof(buffer), "%d", player_context->dy);
+    furi_string_cat_str(json, buffer);
+
+    // closing brace
+    furi_string_cat_str(json, "}");
+
+    // save the json to API
+
+    // create new JSON with username key (of just username), and game_stats key (of the all of the data)
+    FuriString *json_data = furi_string_alloc();
+    if (!json_data)
+    {
+        FURI_LOG_E(TAG, "Failed to allocate JSON string");
+        furi_string_free(json);
+        return false;
+    }
+
+    furi_string_cat_str(json_data, "{\"username\":\"");
+    furi_string_cat_str(json_data, player_context->username);
+    furi_string_cat_str(json_data, "\",\"game_stats\":");
+    furi_string_cat(json_data, json);
+    furi_string_cat_str(json_data, "}");
+
+    furi_string_free(json);
+
+    // save the json_data to the API
+    if (!flipper_http_post_request_with_headers(fhttp, "https://www.flipsocial.net/api/user/update-game-stats/", "{\"Content-Type\":\"application/json\"}", furi_string_get_cstr(json_data)))
+    {
+        FURI_LOG_E(TAG, "Failed to save player context to API");
+        furi_string_free(json_data);
+        return false;
+    }
+    fhttp->state = RECEIVING;
+    while (fhttp->state != IDLE)
+    {
+        furi_delay_ms(100);
+    }
+    furi_string_free(json_data);
+    flipper_http_free(fhttp);
+    return true;
+}
+
 // Helper function to load an integer
 static bool load_number(const char *path_name, int *value)
 {
@@ -549,6 +740,155 @@ bool load_player_context(PlayerContext *player_context)
 
     return true;
 }
+// loads from STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json
+// then gets each key-value pair and saves it as it's own file so it can be loaded separately using
+// load_player_context
+bool set_player_context()
+{
+    char file_path[256];
+    snprintf(file_path, sizeof(file_path),
+             STORAGE_EXT_PATH_PREFIX "/apps_data/flip_world/data/player/player_stats.json");
+
+    FuriString *player_stats = flipper_http_load_from_file(file_path);
+    if (!player_stats)
+    {
+        FURI_LOG_E(TAG, "Failed to load player stats from file: %s", file_path);
+        return false;
+    }
+
+    // Get the key one-by-one and save it to a separate file
+
+    // 1. Username (String)
+    FuriString *username = get_json_value_furi("username", player_stats);
+    if (username)
+    {
+        save_char("player/username", furi_string_get_cstr(username));
+        furi_string_free(username);
+    }
+
+    // 2. Level (uint32_t)
+    FuriString *level = get_json_value_furi("level", player_stats);
+    if (level)
+    {
+        save_uint32("player/level", atoi(furi_string_get_cstr(level)));
+        furi_string_free(level);
+    }
+
+    // 3. XP (uint32_t)
+    FuriString *xp = get_json_value_furi("xp", player_stats);
+    if (xp)
+    {
+        save_uint32("player/xp", atoi(furi_string_get_cstr(xp)));
+        furi_string_free(xp);
+    }
+
+    // 4. Health (uint32_t)
+    FuriString *health = get_json_value_furi("health", player_stats);
+    if (health)
+    {
+        save_uint32("player/health", atoi(furi_string_get_cstr(health)));
+        furi_string_free(health);
+    }
+
+    // 5. Strength (uint32_t)
+    FuriString *strength = get_json_value_furi("strength", player_stats);
+    if (strength)
+    {
+        save_uint32("player/strength", atoi(furi_string_get_cstr(strength)));
+        furi_string_free(strength);
+    }
+
+    // 6. Max Health (uint32_t)
+    FuriString *max_health = get_json_value_furi("max_health", player_stats);
+    if (max_health)
+    {
+        save_uint32("player/max_health", atoi(furi_string_get_cstr(max_health)));
+        furi_string_free(max_health);
+    }
+
+    // 7. Health Regen (uint32_t)
+    FuriString *health_regen = get_json_value_furi("health_regen", player_stats);
+    if (health_regen)
+    {
+        save_uint32("player/health_regen", atoi(furi_string_get_cstr(health_regen)));
+        furi_string_free(health_regen);
+    }
+
+    // 8. Elapsed Health Regen (float)
+    FuriString *elapsed_health_regen = get_json_value_furi("elapsed_health_regen", player_stats);
+    if (elapsed_health_regen)
+    {
+        save_float("player/elapsed_health_regen", strtof(furi_string_get_cstr(elapsed_health_regen), NULL));
+        furi_string_free(elapsed_health_regen);
+    }
+
+    // 9. Attack Timer (float)
+    FuriString *attack_timer = get_json_value_furi("attack_timer", player_stats);
+    if (attack_timer)
+    {
+        save_float("player/attack_timer", strtof(furi_string_get_cstr(attack_timer), NULL));
+        furi_string_free(attack_timer);
+    }
+
+    // 10. Elapsed Attack Timer (float)
+    FuriString *elapsed_attack_timer = get_json_value_furi("elapsed_attack_timer", player_stats);
+    if (elapsed_attack_timer)
+    {
+        save_float("player/elapsed_attack_timer", strtof(furi_string_get_cstr(elapsed_attack_timer), NULL));
+        furi_string_free(elapsed_attack_timer);
+    }
+
+    // 11. Direction (enum PlayerDirection)
+    FuriString *direction = get_json_value_furi("direction", player_stats);
+    if (direction)
+    {
+        save_char("player/direction", furi_string_get_cstr(direction));
+        furi_string_free(direction);
+    }
+
+    // 12. State (enum PlayerState)
+    FuriString *state = get_json_value_furi("state", player_stats);
+    if (state)
+    {
+        save_char("player/state", furi_string_get_cstr(state));
+        furi_string_free(state);
+    }
+
+    // 13. Start Position X (float)
+    FuriString *start_position_x = get_json_value_furi("start_position_x", player_stats);
+    if (start_position_x)
+    {
+        save_float("player/start_position_x", strtof(furi_string_get_cstr(start_position_x), NULL));
+        furi_string_free(start_position_x);
+    }
+
+    // 14. Start Position Y (float)
+    FuriString *start_position_y = get_json_value_furi("start_position_y", player_stats);
+    if (start_position_y)
+    {
+        save_float("player/start_position_y", strtof(furi_string_get_cstr(start_position_y), NULL));
+        furi_string_free(start_position_y);
+    }
+
+    // 15. dx (int8_t)
+    FuriString *dx = get_json_value_furi("dx", player_stats);
+    if (dx)
+    {
+        save_int8("player/dx", atoi(furi_string_get_cstr(dx)));
+        furi_string_free(dx);
+    }
+
+    // 16. dy (int8_t)
+    FuriString *dy = get_json_value_furi("dy", player_stats);
+    if (dy)
+    {
+        save_int8("player/dy", atoi(furi_string_get_cstr(dy)));
+        furi_string_free(dy);
+    }
+
+    furi_string_free(player_stats);
+    return true;
+}
 
 static inline void furi_string_remove_str(FuriString *string, const char *needle)
 {

+ 2 - 0
game/storage.h

@@ -5,7 +5,9 @@
 #include <flip_storage/storage.h>
 
 bool save_player_context(PlayerContext *player_context);
+bool save_player_context_api(PlayerContext *player_context);
 bool load_player_context(PlayerContext *player_context);
+bool set_player_context();
 
 // save the json_data and enemy_data to separate files
 bool separate_world_data(char *id, FuriString *world_data);

+ 1 - 1
game/world.c

@@ -174,7 +174,7 @@ FuriString *fetch_world(const char *name)
     }
 
     char url[256];
-    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v2/get/world/%s/", name);
+    snprintf(url, sizeof(url), "https://www.flipsocial.net/api/world/v3/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(fhttp, url, "{\"Content-Type\": \"application/json\"}"))

+ 18 - 0
jsmn/jsmn.c

@@ -454,6 +454,11 @@ char *get_json_value(char *key, const char *json_data)
         jsmn_parser parser;
         jsmn_init(&parser);
         uint32_t max_tokens = json_token_count(json_data);
+        if (!jsmn_memory_check(max_tokens))
+        {
+            FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+            return NULL;
+        }
         // Allocate tokens array on the heap
         jsmntok_t *tokens = malloc(sizeof(jsmntok_t) * max_tokens);
         if (tokens == NULL)
@@ -572,6 +577,12 @@ char *get_json_array_value(char *key, uint32_t index, const char *json_data)
         return NULL;
     }
     uint32_t max_tokens = json_token_count(array_str);
+    if (!jsmn_memory_check(max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        free(array_str);
+        return NULL;
+    }
 
     jsmn_parser parser;
     jsmn_init(&parser);
@@ -602,6 +613,7 @@ char *get_json_array_value(char *key, uint32_t index, const char *json_data)
 
     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);
         free(array_str);
         return NULL;
@@ -653,6 +665,12 @@ char **get_json_array_values(char *key, char *json_data, int *num_values)
         return NULL;
     }
     uint32_t max_tokens = json_token_count(array_str);
+    if (!jsmn_memory_check(max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        free(array_str);
+        return NULL;
+    }
     // Initialize the JSON parser
     jsmn_parser parser;
     jsmn_init(&parser);

+ 18 - 0
jsmn/jsmn_furi.c

@@ -481,6 +481,11 @@ FuriString *get_json_value_furi(const char *key, const FuriString *json_data)
         return NULL;
     }
     uint32_t max_tokens = json_token_count_furi(json_data);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        return NULL;
+    }
     // Create a temporary FuriString from key
     FuriString *key_str = furi_string_alloc();
     furi_string_cat_str(key_str, key);
@@ -546,6 +551,12 @@ FuriString *get_json_array_value_furi(const char *key, uint32_t index, const Fur
         return NULL;
     }
     uint32_t max_tokens = json_token_count_furi(array_str);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
     jsmn_parser parser;
     jsmn_init_furi(&parser);
 
@@ -576,6 +587,7 @@ FuriString *get_json_array_value_furi(const char *key, uint32_t index, const Fur
 
     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;
@@ -621,6 +633,12 @@ FuriString **get_json_array_values_furi(const char *key, const FuriString *json_
     }
 
     uint32_t max_tokens = json_token_count_furi(array_str);
+    if (!jsmn_memory_check(sizeof(jsmntok_t) * max_tokens))
+    {
+        FURI_LOG_E("JSMM.H", "Insufficient memory for JSON tokens.");
+        furi_string_free(array_str);
+        return NULL;
+    }
     jsmn_parser parser;
     jsmn_init_furi(&parser);
 

+ 1 - 0
jsmn/jsmn_h.c

@@ -12,3 +12,4 @@ FuriString *char_to_furi_string(const char *str)
     }
     return furi_str;
 }
+bool jsmn_memory_check(size_t heap_size) { return memmgr_get_free_heap() > (heap_size + 1024); }

+ 4 - 1
jsmn/jsmn_h.h

@@ -50,4 +50,7 @@ typedef struct
     FuriString *value;
 } FuriJSON;
 
-FuriString *char_to_furi_string(const char *str);
+FuriString *char_to_furi_string(const char *str);
+
+// check memory
+bool jsmn_memory_check(size_t heap_size);