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

added Settings page and some notifications

rdefeo 1 год назад
Родитель
Сommit
bc739b28ff
8 измененных файлов с 428 добавлено и 139 удалено
  1. 2 1
      README.md
  2. 0 0
      assets/tables/95_ex Error.json
  3. 37 0
      notifications.cxx
  4. 8 0
      notifications.h
  5. 278 98
      pinball0.cxx
  6. 13 2
      pinball0.h
  7. 86 35
      table.cxx
  8. 4 3
      table.h

+ 2 - 1
README.md

@@ -1,7 +1,7 @@
 # Pinball0 (Pinball Zero)
 # Pinball0 (Pinball Zero)
 Play pinball on your Flipperzero!
 Play pinball on your Flipperzero!
 
 
-This is a BETA release - like, I'm surprised it works! 
+This is a BETA release - like, I'm surprised it works! You may encounter crashes and/or memory leaks.
 
 
 > The default tables and example tables may / will change
 > The default tables and example tables may / will change
 
 
@@ -17,6 +17,7 @@ This is a BETA release - like, I'm surprised it works!
 * User-defined tables via JSON files
 * User-defined tables via JSON files
 * Portals!
 * Portals!
 * Bumpers, flat surfaces, curved surfaces
 * Bumpers, flat surfaces, curved surfaces
+* Rollover items
 * Fancy animations (-ish)
 * Fancy animations (-ish)
 
 
 ## Controls
 ## Controls

+ 0 - 0
assets/tables/99_ex Error.json → assets/tables/95_ex Error.json


+ 37 - 0
notifications.cxx

@@ -0,0 +1,37 @@
+#include "notifications.h"
+
+static const NotificationMessage* nm_list[16];
+
+void notify_table_bump(PinballApp* app) {
+    int n = 0;
+    if(app->settings.vibrate_enabled) {
+        nm_list[n++] = &message_vibro_on;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_red_255;
+    }
+    nm_list[n++] = &message_delay_100;
+    if(app->settings.vibrate_enabled) {
+        nm_list[n++] = &message_vibro_off;
+    }
+    if(app->settings.led_enabled) {
+        nm_list[n++] = &message_red_0;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}
+
+void notify_error_message(PinballApp* app) {
+    int n = 0;
+    if(app->settings.sound_enabled) {
+        nm_list[n++] = &message_note_c6;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_sound_off;
+        nm_list[n++] = &message_delay_50;
+        nm_list[n++] = &message_note_c5;
+        nm_list[n++] = &message_delay_250;
+        nm_list[n++] = &message_sound_off;
+    }
+    nm_list[n] = NULL;
+    notification_message(app->notify, &nm_list);
+}

+ 8 - 0
notifications.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <furi.h>
+#include <notification/notification.h>
+#include "pinball0.h"
+
+void notify_table_bump(PinballApp* app);
+void notify_error_message(PinballApp* app);

+ 278 - 98
pinball0.cxx

@@ -1,6 +1,9 @@
 #include <furi.h>
 #include <furi.h>
+#include <flipper_format/flipper_format.h>
+#include <notification/notification.h>
 #include <cstring>
 #include <cstring>
 #include "pinball0.h"
 #include "pinball0.h"
+#include "notifications.h"
 
 
 /* generated by fbt from .png files in images folder */
 /* generated by fbt from .png files in images folder */
 #include <pinball0_icons.h>
 #include <pinball0_icons.h>
@@ -13,18 +16,98 @@
 #define GAME_FPS          30
 #define GAME_FPS          30
 #define TABLE_BUMP_AMOUNT 0.3l
 #define TABLE_BUMP_AMOUNT 0.3l
 
 
-#define MANUAL_MODE       true
 #define MANUAL_ADJUSTMENT 20
 #define MANUAL_ADJUSTMENT 20
 
 
-// Sound definitions
-// static const NotificationSequence ns_short_sound = {
-//     &message_note_c5,
-//     &message_delay_50,
-//     &message_sound_off,
-//     NULL,
-// };
+#define PINBALL_SETTINGS_FILENAME     ".pinball0.conf"
+#define PINBALL_SETTINGS_PATH         APP_DATA_PATH(PINBALL_SETTINGS_FILENAME)
+#define PINBALL_SETTINGS_FILE_TYPE    "Pinball0 Settings File"
+#define PINBALL_SETTINGS_FILE_VERSION 1
+
+void pinball_load_settings(PinballApp* pb) {
+    FlipperFormat* fff_settings = flipper_format_file_alloc(pb->storage);
+    FuriString* tmp_str = furi_string_alloc();
+    uint32_t tmp_data32 = 0;
+
+    // init the settings to default values, then overwrite them if
+    // they appear in the settings file
+    pb->settings.sound_enabled = true;
+    pb->settings.led_enabled = false;
+    pb->settings.vibrate_enabled = false;
+    pb->settings.manual_mode = true;
+    pb->selected_setting = 0;
+    pb->max_settings = 4;
+
+    do {
+        if(!flipper_format_file_open_existing(fff_settings, PINBALL_SETTINGS_PATH)) {
+            FURI_LOG_I(TAG, "SETTINGS: File not found, using defaults");
+            break;
+        }
+        if(!flipper_format_read_header(fff_settings, tmp_str, &tmp_data32)) {
+            FURI_LOG_E(TAG, "SETTINGS: Missing or incorrect header");
+            break;
+        }
+        // do settings file version here? eh..
+        if(flipper_format_read_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
+            pb->settings.sound_enabled = (tmp_data32 == 0) ? false : true;
+        }
+        if(flipper_format_read_uint32(fff_settings, "LED", &tmp_data32, 1)) {
+            pb->settings.led_enabled = (tmp_data32 == 0) ? false : true;
+        }
+        if(flipper_format_read_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) {
+            pb->settings.vibrate_enabled = (tmp_data32 == 0) ? false : true;
+        }
+        if(flipper_format_read_uint32(fff_settings, "Manual", &tmp_data32, 1)) {
+            pb->settings.manual_mode = (tmp_data32 == 0) ? false : true;
+        }
+
+    } while(false);
+
+    furi_string_free(tmp_str);
+    flipper_format_free(fff_settings);
+}
+
+void pinball_save_settings(PinballApp* pb) {
+    FlipperFormat* fff_settings = flipper_format_file_alloc(pb->storage);
+    uint32_t tmp_data32 = 0;
+    FURI_LOG_I(TAG, "SETTINGS: Saving settings");
+    do {
+        if(!flipper_format_file_open_always(fff_settings, PINBALL_SETTINGS_PATH)) {
+            FURI_LOG_E(TAG, "SETTINGS: Unable to open file for save!");
+            break;
+        }
+        if(!flipper_format_write_header_cstr(
+               fff_settings, PINBALL_SETTINGS_FILE_TYPE, PINBALL_SETTINGS_FILE_VERSION)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed writing file type and version");
+            break;
+        }
+        // now write out our settings data
+        tmp_data32 = pb->settings.sound_enabled ? 1 : 0;
+        if(!flipper_format_write_uint32(fff_settings, "Sound", &tmp_data32, 1)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Sound'");
+            break;
+        }
+        tmp_data32 = pb->settings.led_enabled ? 1 : 0;
+        if(!flipper_format_write_uint32(fff_settings, "LED", &tmp_data32, 1)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'LED'");
+            break;
+        }
+        tmp_data32 = pb->settings.vibrate_enabled ? 1 : 0;
+        if(!flipper_format_write_uint32(fff_settings, "Vibrate", &tmp_data32, 1)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Vibrate'");
+            break;
+        }
+        tmp_data32 = pb->settings.manual_mode ? 1 : 0;
+        if(!flipper_format_write_uint32(fff_settings, "Manual", &tmp_data32, 1)) {
+            FURI_LOG_E(TAG, "SETTINGS: Failed to write 'Manual'");
+            break;
+        }
+    } while(false);
+
+    flipper_format_file_close(fff_settings);
+    flipper_format_free(fff_settings);
+}
 
 
-void solve(PinballState* pb, float dt) {
+void solve(PinballApp* pb, float dt) {
     Table* table = pb->table;
     Table* table = pb->table;
 
 
     float sub_dt = dt / PHYSICS_SUB_STEPS;
     float sub_dt = dt / PHYSICS_SUB_STEPS;
@@ -125,7 +208,7 @@ void solve(PinballState* pb, float dt) {
     }
     }
 }
 }
 
 
-void pinball_state_init(PinballState* pb) {
+void pinball_app_init(PinballApp* pb) {
     furi_assert(pb);
     furi_assert(pb);
     pb->storage = (Storage*)furi_record_open(RECORD_STORAGE);
     pb->storage = (Storage*)furi_record_open(RECORD_STORAGE);
     pb->notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
     pb->notify = (NotificationApp*)furi_record_open(RECORD_NOTIFICATION);
@@ -139,15 +222,17 @@ void pinball_state_init(PinballState* pb) {
     pb->keys[InputKeyDown] = false;
     pb->keys[InputKeyDown] = false;
     pb->keys[InputKeyRight] = false;
     pb->keys[InputKeyRight] = false;
     pb->keys[InputKeyLeft] = false;
     pb->keys[InputKeyLeft] = false;
-}
 
 
-int modulo(int a, int b) {
-    return (a % b + b) % b;
+    pinball_load_settings(pb);
 }
 }
 
 
+// int modulo(int a, int b) {
+//     return (a % b + b) % b;
+// }
+
 static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
 static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
     furi_assert(ctx);
     furi_assert(ctx);
-    PinballState* pb = (PinballState*)ctx;
+    PinballApp* pb = (PinballApp*)ctx;
     furi_mutex_acquire(pb->mutex, FuriWaitForever);
     furi_mutex_acquire(pb->mutex, FuriWaitForever);
 
 
     // What are we drawing? table select / menu or the actual game?
     // What are we drawing? table select / menu or the actual game?
@@ -158,9 +243,9 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
         // and the currently selected item is always in the middle, surrounded by pinballs
         // and the currently selected item is always in the middle, surrounded by pinballs
         const TableList& list = pb->table_list;
         const TableList& list = pb->table_list;
         int32_t y = 25;
         int32_t y = 25;
-        size_t half_way = list.display_size / 2;
+        auto half_way = list.display_size / 2;
 
 
-        for(size_t i = 0; i < list.display_size; i++) {
+        for(auto i = 0; i < list.display_size; i++) {
             int index =
             int index =
                 (list.selected - half_way + i + list.menu_items.size()) % list.menu_items.size();
                 (list.selected - half_way + i + list.menu_items.size()) % list.menu_items.size();
             const auto& menu_item = list.menu_items[index];
             const auto& menu_item = list.menu_items[index];
@@ -236,6 +321,53 @@ static void pinball_draw_callback(Canvas* const canvas, void* ctx) {
 
 
         pb->table->draw(canvas);
         pb->table->draw(canvas);
     } break;
     } break;
+    case GM_Settings: {
+        canvas_draw_str_aligned(canvas, 2, 10, AlignLeft, AlignTop, "SETTINGS");
+
+        int x = 55;
+        int y = 30;
+
+        canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Sound");
+        canvas_draw_circle(canvas, x, y + 3, 4);
+        if(pb->settings.sound_enabled) {
+            canvas_draw_disc(canvas, x, y + 3, 2);
+        }
+        if(pb->selected_setting == 0) {
+            canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
+        }
+        y += 12;
+
+        canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "LED");
+        canvas_draw_circle(canvas, x, y + 3, 4);
+        if(pb->settings.led_enabled) {
+            canvas_draw_disc(canvas, x, y + 3, 2);
+        }
+        if(pb->selected_setting == 1) {
+            canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
+        }
+        y += 12;
+
+        canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Vibrate");
+        canvas_draw_circle(canvas, x, y + 3, 4);
+        if(pb->settings.vibrate_enabled) {
+            canvas_draw_disc(canvas, x, y + 3, 2);
+        }
+        if(pb->selected_setting == 2) {
+            canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
+        }
+        y += 12;
+
+        canvas_draw_str_aligned(canvas, 10, y, AlignLeft, AlignTop, "Manual");
+        canvas_draw_circle(canvas, x, y + 3, 4);
+        if(pb->settings.manual_mode) {
+            canvas_draw_disc(canvas, x, y + 3, 2);
+        }
+        if(pb->selected_setting == 3) {
+            canvas_draw_triangle(canvas, 2, y + 3, 8, 5, CanvasDirectionLeftToRight);
+        }
+
+        pb->table->draw(canvas);
+    } break;
     default:
     default:
         FURI_LOG_E(TAG, "Unknown Game Mode");
         FURI_LOG_E(TAG, "Unknown Game Mode");
         break;
         break;
@@ -254,49 +386,49 @@ static void pinball_input_callback(InputEvent* input_event, void* ctx) {
 extern "C" int32_t pinball0_app(void* p) {
 extern "C" int32_t pinball0_app(void* p) {
     UNUSED(p);
     UNUSED(p);
 
 
-    PinballState* pinball_state = (PinballState*)malloc(sizeof(PinballState));
-    pinball_state->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
-    if(!pinball_state->mutex) {
+    PinballApp* app = (PinballApp*)malloc(sizeof(PinballApp));
+    app->mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    if(!app->mutex) {
         FURI_LOG_E(TAG, "Cannot create mutex!");
         FURI_LOG_E(TAG, "Cannot create mutex!");
-        free(pinball_state);
+        free(app);
         return 0;
         return 0;
     }
     }
 
 
-    pinball_state_init(pinball_state);
+    pinball_app_init(app);
 
 
     // read the list of tables from storage
     // read the list of tables from storage
-    table_table_list_init(pinball_state);
+    table_table_list_init(app);
 
 
     // load the table select table
     // load the table select table
-    table_load_table(pinball_state, 0);
+    table_load_table(app, 0);
 
 
     FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PinballEvent));
     FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(PinballEvent));
     furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
     furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
 
 
     ViewPort* view_port = view_port_alloc();
     ViewPort* view_port = view_port_alloc();
     view_port_set_orientation(view_port, ViewPortOrientationVertical);
     view_port_set_orientation(view_port, ViewPortOrientationVertical);
-    view_port_draw_callback_set(view_port, pinball_draw_callback, pinball_state);
+    view_port_draw_callback_set(view_port, pinball_draw_callback, app);
     view_port_input_callback_set(view_port, pinball_input_callback, event_queue);
     view_port_input_callback_set(view_port, pinball_input_callback, event_queue);
 
 
     // Open the GUI and register view_port
     // Open the GUI and register view_port
     Gui* gui = (Gui*)furi_record_open(RECORD_GUI);
     Gui* gui = (Gui*)furi_record_open(RECORD_GUI);
     gui_add_view_port(gui, view_port, GuiLayerFullscreen);
     gui_add_view_port(gui, view_port, GuiLayerFullscreen);
 
 
-    notification_message(pinball_state->notify, &sequence_display_backlight_enforce_on);
+    notification_message(app->notify, &sequence_display_backlight_enforce_on);
 
 
     // dolphin_deed(DolphinDeedPluginGameStart);
     // dolphin_deed(DolphinDeedPluginGameStart);
 
 
-    pinball_state->processing = true;
+    app->processing = true;
 
 
     float dt = 0.0f;
     float dt = 0.0f;
     uint32_t last_frame_time = furi_get_tick();
     uint32_t last_frame_time = furi_get_tick();
 
 
     FURI_LOG_I(TAG, "Starting event loop");
     FURI_LOG_I(TAG, "Starting event loop");
     PinballEvent event;
     PinballEvent event;
-    while(pinball_state->processing) {
+    while(app->processing) {
         FuriStatus event_status =
         FuriStatus event_status =
             furi_message_queue_get(event_queue, &event, 10); // TODO best rate?
             furi_message_queue_get(event_queue, &event, 10); // TODO best rate?
-        furi_mutex_acquire(pinball_state->mutex, FuriWaitForever);
+        furi_mutex_acquire(app->mutex, FuriWaitForever);
 
 
         if(event_status == FuriStatusOk) {
         if(event_status == FuriStatusOk) {
             if(event.type == EventTypeKey) {
             if(event.type == EventTypeKey) {
@@ -304,87 +436,135 @@ extern "C" int32_t pinball0_app(void* p) {
                    event.input.type == InputTypeRepeat) {
                    event.input.type == InputTypeRepeat) {
                     switch(event.input.key) {
                     switch(event.input.key) {
                     case InputKeyBack:
                     case InputKeyBack:
-                        if(pinball_state->game_mode == GM_Playing ||
-                           pinball_state->game_mode == GM_GameOver ||
-                           pinball_state->game_mode == GM_Error) {
-                            pinball_state->game_mode = GM_TableSelect;
-                            table_load_table(pinball_state, TABLE_SELECT);
-                        } else if(pinball_state->game_mode == GM_TableSelect) {
-                            pinball_state->processing = false;
+                        if(app->game_mode == GM_Playing || app->game_mode == GM_GameOver ||
+                           app->game_mode == GM_Error || app->game_mode == GM_Settings) {
+                            if(app->game_mode == GM_Settings) {
+                                pinball_save_settings(app);
+                            }
+                            app->game_mode = GM_TableSelect;
+                            table_load_table(app, TABLE_SELECT);
+                        } else if(app->game_mode == GM_TableSelect) {
+                            app->processing = false;
                         }
                         }
                         break;
                         break;
                     case InputKeyRight:
                     case InputKeyRight:
-                        pinball_state->keys[InputKeyRight] = true;
-                        // temp
-                        if(MANUAL_MODE && pinball_state->table->balls_released == false) {
-                            pinball_state->table->balls[0].p.x += MANUAL_ADJUSTMENT;
-                            pinball_state->table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
+                        app->keys[InputKeyRight] = true;
+
+                        if(app->settings.manual_mode && app->table->balls_released == false) {
+                            app->table->balls[0].p.x += MANUAL_ADJUSTMENT;
+                            app->table->balls[0].prev_p.x += MANUAL_ADJUSTMENT;
                         }
                         }
-                        for(auto& f : pinball_state->table->flippers) {
+                        for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::RIGHT) {
                             if(f.side == Flipper::RIGHT) {
                                 f.powered = true;
                                 f.powered = true;
                             }
                             }
                         }
                         }
                         break;
                         break;
                     case InputKeyLeft:
                     case InputKeyLeft:
-                        // FURI_LOG_I(TAG, "LEFT on");
-                        pinball_state->keys[InputKeyLeft] = true;
-                        // temp
-                        if(MANUAL_MODE && pinball_state->table->balls_released == false) {
-                            pinball_state->table->balls[0].p.x -= MANUAL_ADJUSTMENT;
-                            pinball_state->table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
+                        app->keys[InputKeyLeft] = true;
+
+                        if(app->settings.manual_mode && app->table->balls_released == false) {
+                            app->table->balls[0].p.x -= MANUAL_ADJUSTMENT;
+                            app->table->balls[0].prev_p.x -= MANUAL_ADJUSTMENT;
                         }
                         }
-                        for(auto& f : pinball_state->table->flippers) {
+                        for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::LEFT) {
                             if(f.side == Flipper::LEFT) {
                                 f.powered = true;
                                 f.powered = true;
                             }
                             }
                         }
                         }
                         break;
                         break;
-                    case InputKeyUp: // bump table
-                        if(pinball_state->game_mode == GM_Playing) {
+                    case InputKeyUp:
+                        switch(app->game_mode) {
+                        case GM_Playing:
                             if(event.input.type == InputTypePress) {
                             if(event.input.type == InputTypePress) {
-                                pinball_state->keys[InputKeyUp] = true;
+                                // we only set the key if it's a 'press' to ensure
+                                // a single table "bump"
+                                app->keys[InputKeyUp] = true;
+                                notify_table_bump(app);
                             }
                             }
-                        } else if(pinball_state->game_mode == GM_TableSelect) {
-                            pinball_state->table_list.selected =
-                                (pinball_state->table_list.selected - 1 +
-                                 pinball_state->table_list.menu_items.size()) %
-                                pinball_state->table_list.menu_items.size();
-                        }
-                        // temp
-                        if(MANUAL_MODE && pinball_state->table->balls_released == false) {
-                            pinball_state->table->balls[0].p.y -= MANUAL_ADJUSTMENT;
-                            pinball_state->table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT;
+                            if(app->settings.manual_mode && app->table->balls_released == false) {
+                                app->table->balls[0].p.y -= MANUAL_ADJUSTMENT;
+                                app->table->balls[0].prev_p.y -= MANUAL_ADJUSTMENT;
+                            }
+                            break;
+                        case GM_TableSelect:
+                            app->table_list.selected = (app->table_list.selected - 1 +
+                                                        app->table_list.menu_items.size()) %
+                                                       app->table_list.menu_items.size();
+                            break;
+                        case GM_Settings:
+                            if(app->selected_setting > 0) {
+                                app->selected_setting--;
+                            }
+                            break;
+                        default:
+                            break;
                         }
                         }
                         break;
                         break;
                     case InputKeyDown:
                     case InputKeyDown:
-                        if(pinball_state->game_mode == GM_Playing) {
-                            pinball_state->keys[InputKeyDown] = true;
-                        } else if(pinball_state->game_mode == GM_TableSelect) {
-                            pinball_state->table_list.selected =
-                                (pinball_state->table_list.selected + 1) %
-                                pinball_state->table_list.menu_items.size();
-                        }
-                        // temp
-                        if(MANUAL_MODE && pinball_state->table->balls_released == false) {
-                            pinball_state->table->balls[0].p.y += MANUAL_ADJUSTMENT;
-                            pinball_state->table->balls[0].prev_p.y += MANUAL_ADJUSTMENT;
+                        switch(app->game_mode) {
+                        case GM_Playing:
+                            app->keys[InputKeyDown] = true;
+                            if(app->settings.manual_mode && app->table->balls_released == false) {
+                                app->table->balls[0].p.y += MANUAL_ADJUSTMENT;
+                                app->table->balls[0].prev_p.y += MANUAL_ADJUSTMENT;
+                            }
+                            break;
+                        case GM_TableSelect:
+                            app->table_list.selected = (app->table_list.selected + 1 +
+                                                        app->table_list.menu_items.size()) %
+                                                       app->table_list.menu_items.size();
+                            break;
+                        case GM_Settings:
+                            if(app->selected_setting < app->max_settings - 1) {
+                                app->selected_setting++;
+                            }
+                            break;
+                        default:
+                            break;
                         }
                         }
                         break;
                         break;
                     case InputKeyOk:
                     case InputKeyOk:
-                        if(pinball_state->game_mode == GM_Playing) {
-                            if(!pinball_state->table->balls_released) {
-                                pinball_state->gameStarted = true;
-                                pinball_state->table->balls_released = true;
+                        switch(app->game_mode) {
+                        case GM_Playing:
+                            if(!app->table->balls_released) {
+                                app->gameStarted = true;
+                                app->table->balls_released = true;
                             }
                             }
-                        } else if(pinball_state->game_mode == GM_TableSelect) {
-                            size_t sel = pinball_state->table_list.selected;
-                            if(!table_load_table(pinball_state, sel + TABLE_INDEX_OFFSET)) {
-                                pinball_state->game_mode = GM_Error;
-                                table_load_table(pinball_state, TABLE_ERROR);
+                            break;
+                        case GM_TableSelect: {
+                            size_t sel = app->table_list.selected;
+                            if(sel == app->table_list.menu_items.size() - 1) {
+                                app->game_mode = GM_Settings;
+                                table_load_table(app, TABLE_SETTINGS);
+                            } else if(!table_load_table(app, sel + TABLE_INDEX_OFFSET)) {
+                                app->game_mode = GM_Error;
+                                table_load_table(app, TABLE_ERROR);
+                                notify_error_message(app);
                             } else {
                             } else {
-                                pinball_state->game_mode = GM_Playing;
+                                app->game_mode = GM_Playing;
+                            }
+                        } break;
+                        case GM_Settings:
+                            switch(app->selected_setting) {
+                            case 0:
+                                app->settings.sound_enabled = !app->settings.sound_enabled;
+                                break;
+                            case 1:
+                                app->settings.led_enabled = !app->settings.led_enabled;
+                                break;
+                            case 2:
+                                app->settings.vibrate_enabled = !app->settings.vibrate_enabled;
+                                break;
+                            case 3:
+                                app->settings.manual_mode = !app->settings.manual_mode;
+                                break;
+                            default:
+                                break;
                             }
                             }
+                            break;
+                        default:
+                            break;
                         }
                         }
                         break;
                         break;
                     default:
                     default:
@@ -393,8 +573,8 @@ extern "C" int32_t pinball0_app(void* p) {
                 } else if(event.input.type == InputTypeRelease) {
                 } else if(event.input.type == InputTypeRelease) {
                     switch(event.input.key) {
                     switch(event.input.key) {
                     case InputKeyLeft: {
                     case InputKeyLeft: {
-                        pinball_state->keys[InputKeyLeft] = false;
-                        for(auto& f : pinball_state->table->flippers) {
+                        app->keys[InputKeyLeft] = false;
+                        for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::LEFT) {
                             if(f.side == Flipper::LEFT) {
                                 f.powered = false;
                                 f.powered = false;
                             }
                             }
@@ -402,8 +582,8 @@ extern "C" int32_t pinball0_app(void* p) {
                         break;
                         break;
                     }
                     }
                     case InputKeyRight: {
                     case InputKeyRight: {
-                        pinball_state->keys[InputKeyRight] = false;
-                        for(auto& f : pinball_state->table->flippers) {
+                        app->keys[InputKeyRight] = false;
+                        for(auto& f : app->table->flippers) {
                             if(f.side == Flipper::RIGHT) {
                             if(f.side == Flipper::RIGHT) {
                                 f.powered = false;
                                 f.powered = false;
                             }
                             }
@@ -411,10 +591,10 @@ extern "C" int32_t pinball0_app(void* p) {
                         break;
                         break;
                     }
                     }
                     case InputKeyUp:
                     case InputKeyUp:
-                        pinball_state->keys[InputKeyUp] = false;
+                        app->keys[InputKeyUp] = false;
                         break;
                         break;
                     case InputKeyDown:
                     case InputKeyDown:
-                        pinball_state->keys[InputKeyDown] = false;
+                        app->keys[InputKeyDown] = false;
                         // TODO: release plunger?
                         // TODO: release plunger?
                         break;
                         break;
                     default:
                     default:
@@ -423,19 +603,19 @@ extern "C" int32_t pinball0_app(void* p) {
                 }
                 }
             }
             }
         }
         }
-        solve(pinball_state, dt);
-        for(auto& o : pinball_state->table->objects) {
+        solve(app, dt);
+        for(auto& o : app->table->objects) {
             o->step_animation();
             o->step_animation();
         }
         }
         // check game state
         // check game state
-        if(pinball_state->game_mode == GM_Playing && pinball_state->table->num_lives == 0) {
+        if(app->game_mode == GM_Playing && app->table->num_lives == 0) {
             FURI_LOG_W(TAG, "GAME OVER!");
             FURI_LOG_W(TAG, "GAME OVER!");
-            pinball_state->game_mode = GM_GameOver;
+            app->game_mode = GM_GameOver;
         }
         }
 
 
         // no keys pressed - we should clear all input keys?
         // no keys pressed - we should clear all input keys?
         view_port_update(view_port);
         view_port_update(view_port);
-        furi_mutex_release(pinball_state->mutex);
+        furi_mutex_release(app->mutex);
 
 
         // game timing
         // game timing
         uint32_t time_lapsed = furi_get_tick() - last_frame_time;
         uint32_t time_lapsed = furi_get_tick() - last_frame_time;
@@ -444,12 +624,12 @@ extern "C" int32_t pinball0_app(void* p) {
             time_lapsed = furi_get_tick() - last_frame_time;
             time_lapsed = furi_get_tick() - last_frame_time;
             dt = time_lapsed / 1000.0f;
             dt = time_lapsed / 1000.0f;
         }
         }
-        pinball_state->tick++;
+        app->tick++;
         last_frame_time = furi_get_tick();
         last_frame_time = furi_get_tick();
     }
     }
 
 
     // general cleanup
     // general cleanup
-    notification_message(pinball_state->notify, &sequence_display_backlight_enforce_auto);
+    notification_message(app->notify, &sequence_display_backlight_enforce_auto);
 
 
     view_port_enabled_set(view_port, false);
     view_port_enabled_set(view_port, false);
     gui_remove_view_port(gui, view_port);
     gui_remove_view_port(gui, view_port);
@@ -459,10 +639,10 @@ extern "C" int32_t pinball0_app(void* p) {
     view_port_free(view_port);
     view_port_free(view_port);
     furi_message_queue_free(event_queue);
     furi_message_queue_free(event_queue);
 
 
-    furi_mutex_free(pinball_state->mutex);
+    furi_mutex_free(app->mutex);
 
 
-    delete pinball_state->table;
-    free(pinball_state);
+    delete app->table;
+    free(app);
 
 
     furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
     furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
     return 0;
     return 0;

+ 13 - 2
pinball0.h

@@ -40,7 +40,8 @@ typedef enum GameMode {
     GM_Playing,
     GM_Playing,
     GM_GameOver,
     GM_GameOver,
     GM_Error,
     GM_Error,
-    GM_About // TODO
+    GM_Settings,
+    GM_About
 } GameMode;
 } GameMode;
 
 
 typedef struct {
 typedef struct {
@@ -56,9 +57,19 @@ typedef struct {
     bool keys[4]; // which key was pressed?
     bool keys[4]; // which key was pressed?
     bool processing; // controls game loop and physics threads
     bool processing; // controls game loop and physics threads
 
 
+    // user settings
+    struct {
+        bool sound_enabled;
+        bool vibrate_enabled;
+        bool led_enabled;
+        bool manual_mode;
+    } settings;
+    int selected_setting;
+    int max_settings;
+
     // system objects
     // system objects
     Storage* storage;
     Storage* storage;
     NotificationApp* notify; // allows us to blink/buzz during game
     NotificationApp* notify; // allows us to blink/buzz during game
     char text[256]; // general temp buffer
     char text[256]; // general temp buffer
 
 
-} PinballState;
+} PinballApp;

+ 86 - 35
table.cxx

@@ -12,10 +12,6 @@
 #define LIVES     3
 #define LIVES     3
 #define LIVES_POS Vec2(20, 20)
 #define LIVES_POS Vec2(20, 20)
 
 
-// forward declares
-Table* table_init_table_select(void* ctx);
-Table* table_init_table_error(void* ctx);
-
 Table::~Table() {
 Table::~Table() {
     for(size_t i = 0; i < objects.size(); i++) {
     for(size_t i = 0; i < objects.size(); i++) {
         delete objects[i];
         delete objects[i];
@@ -60,7 +56,7 @@ void Table::draw(Canvas* canvas) {
 }
 }
 
 
 void table_table_list_init(void* ctx) {
 void table_table_list_init(void* ctx) {
-    PinballState* pb = (PinballState*)ctx;
+    PinballApp* pb = (PinballApp*)ctx;
     // using the asset file path, read the table files, and for each one, extract their
     // using the asset file path, read the table files, and for each one, extract their
     // display name (oof). let's just use their filenames for now (stripping any XX_ prefix)
     // display name (oof). let's just use their filenames for now (stripping any XX_ prefix)
     // sort tables by original filename
     // sort tables by original filename
@@ -118,6 +114,12 @@ void table_table_list_init(void* ctx) {
         dir_walk_free(dir_walk);
         dir_walk_free(dir_walk);
     }
     }
 
 
+    // Add 'Settings' as last element
+    TableMenuItem settings;
+    settings.filename = furi_string_alloc_set_str("99_Settings");
+    settings.name = furi_string_alloc_set_str("SETTINGS");
+    pb->table_list.menu_items.push_back(settings);
+
     FURI_LOG_I(TAG, "Found %d tables", pb->table_list.menu_items.size());
     FURI_LOG_I(TAG, "Found %d tables", pb->table_list.menu_items.size());
     for(auto& tmi : pb->table_list.menu_items) {
     for(auto& tmi : pb->table_list.menu_items) {
         FURI_LOG_I(TAG, "%s", furi_string_get_cstr(tmi.name));
         FURI_LOG_I(TAG, "%s", furi_string_get_cstr(tmi.name));
@@ -154,7 +156,7 @@ bool table_file_parse_float(const nx_json* json, const char* key, float* v) {
     return true;
     return true;
 }
 }
 
 
-Table* table_load_table_from_file(PinballState* pb, size_t index) {
+Table* table_load_table_from_file(PinballApp* pb, size_t index) {
     auto& tmi = pb->table_list.menu_items[index];
     auto& tmi = pb->table_list.menu_items[index];
 
 
     FURI_LOG_I(TAG, "Reading file: %s", furi_string_get_cstr(tmi.filename));
     FURI_LOG_I(TAG, "Reading file: %s", furi_string_get_cstr(tmi.filename));
@@ -485,7 +487,7 @@ Table* table_load_table_from_file(PinballState* pb, size_t index) {
             }
             }
         }
         }
         break;
         break;
-    } while(true);
+    } while(false);
 
 
     nx_json_free(json);
     nx_json_free(json);
     free(json_buffer);
     free(json_buffer);
@@ -493,33 +495,6 @@ Table* table_load_table_from_file(PinballState* pb, size_t index) {
     return table;
     return table;
 }
 }
 
 
-bool table_load_table(void* ctx, size_t index) {
-    PinballState* pb = (PinballState*)ctx;
-
-    // read the index'th file in pb->table_list and allocate
-    FURI_LOG_I(TAG, "Loading table %u", index);
-
-    // if there's already a table loaded, free it
-    if(pb->table) {
-        delete pb->table;
-        pb->table = nullptr;
-    }
-
-    pb->gameStarted = false;
-    switch(index) {
-    case TABLE_SELECT:
-        pb->table = table_init_table_select(ctx);
-        break;
-    case TABLE_ERROR:
-        pb->table = table_init_table_error(ctx);
-        break;
-    default:
-        pb->table = table_load_table_from_file(pb, index - TABLE_INDEX_OFFSET);
-        break;
-    }
-    return pb->table != NULL;
-}
-
 TableList::~TableList() {
 TableList::~TableList() {
     for(auto& mi : menu_items) {
     for(auto& mi : menu_items) {
         furi_string_free(mi.name);
         furi_string_free(mi.name);
@@ -579,7 +554,7 @@ Table* table_init_table_select(void* ctx) {
 
 
 Table* table_init_table_error(void* ctx) {
 Table* table_init_table_error(void* ctx) {
     UNUSED(ctx);
     UNUSED(ctx);
-    // PinballState* pb = (PinballState*)ctx;
+    // PinballApp* pb = (PinballApp*)ctx;
     Table* table = new Table();
     Table* table = new Table();
 
 
     table->balls.push_back(Ball(Vec2(20, 880), 30));
     table->balls.push_back(Ball(Vec2(20, 880), 30));
@@ -623,3 +598,79 @@ Table* table_init_table_error(void* ctx) {
 
 
     return table;
     return table;
 }
 }
+
+Table* table_init_table_settings(void* ctx) {
+    UNUSED(ctx);
+    Table* table = new Table();
+
+    table->balls.push_back(Ball(Vec2(20, 880), 10));
+    table->balls.back().add_velocity(Vec2(7, 0), .10f);
+    table->balls.push_back(Ball(Vec2(610, 920), 10));
+    table->balls.back().add_velocity(Vec2(-8, 0), .10f);
+    table->balls.push_back(Ball(Vec2(250, 980), 10));
+    table->balls.back().add_velocity(Vec2(10, 0), .10f);
+
+    table->balls_released = true;
+
+    Polygon* new_rail = new Polygon();
+    new_rail->add_point({-1, 840});
+    new_rail->add_point({-1, 1280});
+    new_rail->finalize();
+    new_rail->hidden = true;
+    table->objects.push_back(new_rail);
+
+    new_rail = new Polygon();
+    new_rail->add_point({-1, 1280});
+    new_rail->add_point({640, 1280});
+    new_rail->finalize();
+    new_rail->hidden = true;
+    table->objects.push_back(new_rail);
+
+    new_rail = new Polygon();
+    new_rail->add_point({640, 1280});
+    new_rail->add_point({640, 840});
+    new_rail->finalize();
+    new_rail->hidden = true;
+    table->objects.push_back(new_rail);
+
+    // int gap = 8;
+    // int speed = 3;
+    // float top = 20;
+
+    // table->objects.push_back(new Chaser(Vec2(2, top), Vec2(61, top), gap, speed, Chaser::SLASH));
+    // table->objects.push_back(new Chaser(Vec2(2, top), Vec2(2, 84), gap, speed, Chaser::SLASH));
+    // table->objects.push_back(new Chaser(Vec2(2, 84), Vec2(61, 84), gap, speed, Chaser::SLASH));
+    // table->objects.push_back(new Chaser(Vec2(61, top), Vec2(61, 84), gap, speed, Chaser::SLASH));
+
+    return table;
+}
+
+bool table_load_table(void* ctx, size_t index) {
+    PinballApp* pb = (PinballApp*)ctx;
+
+    // read the index'th file in pb->table_list and allocate
+    FURI_LOG_I(TAG, "Loading table %u", index);
+
+    // if there's already a table loaded, free it
+    if(pb->table) {
+        delete pb->table;
+        pb->table = nullptr;
+    }
+
+    pb->gameStarted = false;
+    switch(index) {
+    case TABLE_SELECT:
+        pb->table = table_init_table_select(ctx);
+        break;
+    case TABLE_ERROR:
+        pb->table = table_init_table_error(ctx);
+        break;
+    case TABLE_SETTINGS:
+        pb->table = table_init_table_settings(ctx);
+        break;
+    default:
+        pb->table = table_load_table_from_file(pb, index - TABLE_INDEX_OFFSET);
+        break;
+    }
+    return pb->table != NULL;
+}

+ 4 - 3
table.h

@@ -6,7 +6,8 @@
 
 
 #define TABLE_SELECT       0
 #define TABLE_SELECT       0
 #define TABLE_ERROR        1
 #define TABLE_ERROR        1
-#define TABLE_INDEX_OFFSET 2
+#define TABLE_SETTINGS     2
+#define TABLE_INDEX_OFFSET 3
 
 
 // Defines all of the elements on a pinball table:
 // Defines all of the elements on a pinball table:
 // edges, bumpers, flipper locations, scoreboard
 // edges, bumpers, flipper locations, scoreboard
@@ -44,8 +45,8 @@ class TableList {
 public:
 public:
     ~TableList();
     ~TableList();
     std::vector<TableMenuItem> menu_items;
     std::vector<TableMenuItem> menu_items;
-    size_t display_size; // how many can fit on screen
-    size_t selected;
+    int display_size; // how many can fit on screen
+    int selected;
 };
 };
 
 
 // Read the list tables from the data folder and store in the state
 // Read the list tables from the data folder and store in the state