rdefeo 1 год назад
Родитель
Сommit
1d547036a3
11 измененных файлов с 428 добавлено и 55 удалено
  1. 38 0
      README.md
  2. 84 0
      assets/tables/00_dbg Flip0.json
  3. 54 4
      assets/tables/02_Classic.json
  4. 0 1
      notifications.cxx
  5. 36 43
      objects.cxx
  6. 15 5
      objects.h
  7. 3 0
      pinball0.cxx
  8. 118 0
      signals.cxx
  9. 32 0
      signals.h
  10. 3 0
      table.h
  11. 45 2
      table_parser.cxx

+ 38 - 0
README.md

@@ -125,6 +125,44 @@ Defines two portals, **a** and **b**. They are bi-drectional. Like rails, their
 
 When the ball hits a turbo boost, it will force the new velocity to be in the direction of `angle`.
 
+#### signal : object attribute (optional)
+* `"tx": N` : id to transmit
+* `"rx": N` : id to receive
+* `"any": bool` : signal trigger type, defaults to `false` (i.e. "all")
+
+Can be added to a **bumper** or **rollover** to send notifications of collisions. When defining a signal, you can define a `tx` or a `rx` or both. The id value can be any number. If you add a `"tx": 4` to a rollover, when the ball collides with the rollover, it will trigger a signal to any object that has a `"rx": 4` defined.
+
+The `"any"` bool will determine __when__ to send the signal. Let's look at an example:
+
+```json
+"rollovers": [
+    {
+        "position": [ X, Y ],
+        "symbol" : "A",
+        "signal": { "tx": 3 }
+    },
+    {
+        "position": [ X, Y ],
+        "symbol" : "B",
+        "signal": { "tx": 3 }
+    }
+]
+```
+
+Since we didn't specify `"any": true` within the `signal` objects for each rollover, then ALL rollovers must be hit by the ball before a signal for id 3 is "sent". If we specifyed `"any": true`, then the signal with id 3 will be sent when the ball hits any of the rollovers.
+
+All signals with a `tx` must have a corresponding `rx` with the same id on the table - and vice versa - otherwise an error will be thrown.
+
+All signals with the same `tx` id must have the same trigger type (i.e. any or all).
+
+The default behavior for objects when a signal is received / sent:
+* **rollover**
+  * received - rollover reset to non-activated (i.e. symbol is hidden)
+  * sent - nothing
+* **bumper**
+  * received - becomes visible and can be hit by ball
+  * sent - disappears
+
 #### tilt_detect : boolean
 * `"tilt_detect": bool` : optional, defaults to `true`
 

+ 84 - 0
assets/tables/00_dbg Flip0.json

@@ -0,0 +1,84 @@
+{
+    "name": "Flip0",
+    "lives": {
+        "display": false,
+        "position": [ 400, 20 ]
+    },
+    "score": {
+        "display": false,
+        "position": [ 23, 0 ]
+    },
+    "balls": [
+        {
+            "position": [ 30, 1110 ],
+            "velocity": [ 0, -12.0 ]
+        }
+    ],
+    "flippers": [
+        {
+            "position": [ 130, 1200 ],
+            "side": "LEFT",
+            "size": 130
+        },
+        {
+            "position": [ 490, 1200 ],
+            "side": "RIGHT",
+            "size": 130
+        }
+    ],
+    "arcs": [
+        {
+            // peak
+            "position": [ 250, 100 ],
+            "radius": 100,
+            "start_angle": 60,
+            "end_angle": 120,
+            "surface": "INSIDE"
+        },
+        {
+            // top left corner
+            "position": [ 100, 220 ],
+            "radius": 100,
+            "start_angle": 120,
+            "end_angle": 180,
+            "surface": "INSIDE"
+        },
+        {
+            // top right corner
+            "position": [ 540, 330 ],
+            "radius": 100,
+            "start_angle": 0,
+            "end_angle": 50,
+            "surface": "INSIDE"
+        }
+
+    ],
+    "rails": [
+        // top right roof
+        {
+            "start": [ 604, 253 ],
+            "end": [ 300, 13 ]
+        },
+        // top left roof
+        {
+            "start": [ 200, 13 ],
+            "end": [ 50, 133 ]
+        },
+        // left wall
+        {
+            "start": [ 0, 240 ],
+            "end": [ 0, 1200 ]
+        },
+        // left wall rail
+        {
+            "start": [ 70, 240 ],
+            "end": [ 70, 1100 ],
+            "double_sided": true
+        },
+        // right wall
+        {
+            "start": [ 630, 1100 ],
+            "end": [ 630, 330 ]
+        }
+    ]
+}

+ 54 - 4
assets/tables/02_Classic.json

@@ -43,6 +43,40 @@
         {
             "position": [ 480, 500 ],
             "radius": 40
+        },
+        // gutter guards - for Z,E,R,O rollover
+        {
+            "position": [ 310, 1265 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
+        },
+        {
+            "position": [ 35, 900 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
+        },
+        {
+            "position": [ 595, 900 ],
+            "radius": 20,
+            "physical": false,
+            "hidden": true,
+            "signal": {
+                "tx": 1,
+                "rx": 0,
+                "any": true
+            }
         }
     ],
     "arcs": [
@@ -125,19 +159,35 @@
     "rollovers": [
         {
             "position": [ 200, 800 ],
-            "symbol": "Z"
+            "symbol": "Z",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 280, 770 ],
-            "symbol": "E"
+            "symbol": "E",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 360, 770 ],
-            "symbol": "R"
+            "symbol": "R",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         },
         {
             "position": [ 440, 800 ],
-            "symbol": "O"
+            "symbol": "O",
+            "signal": {
+                "tx": 0,
+                "rx": 1
+            }
         }
     ]
 }

+ 0 - 1
notifications.cxx

@@ -120,7 +120,6 @@ void notify_game_over(void* ctx) {
 }
 
 void notify_bumper_hit(void* ctx) {
-    FURI_LOG_I(TAG, "notify_bumper_hit");
     PinballApp* app = (PinballApp*)ctx;
     int n = 0;
     if(app->settings.led_enabled) {

+ 36 - 43
objects.cxx

@@ -117,14 +117,14 @@ bool Flipper::collide(Ball& ball) {
     perp *= -1.0f;
     perp.normalize();
     Vec2 surface_velocity = perp * 1.7f; // TODO: flipper power??
-    FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
+    // FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
     if(current_omega != 0.0f) surface_velocity *= current_omega;
-    FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
+    // FURI_LOG_I(TAG, "sv: %.3f,%.3f", (double)surface_velocity.x, (double)surface_velocity.y);
 
     // TODO: Flippers currently aren't "bouncy" when they are still
     float v = ball_v.dot(dir);
     float v_new = surface_velocity.dot(dir);
-    FURI_LOG_I(TAG, "v_new: %.4f, v: %.4f", (double)v_new, (double)v);
+    // FURI_LOG_I(TAG, "v_new: %.4f, v: %.4f", (double)v_new, (double)v);
     ball_v += dir * (v_new - v);
     ball.prev_p = ball.p - ball_v;
     return true;
@@ -137,6 +137,18 @@ Vec2 Flipper::get_tip() const {
     return tip;
 }
 
+// The default action for receiving a signal is to "appear"
+void FixedObject::signal_receive() {
+    physical = true;
+    hidden = false;
+}
+
+// The default action for a sending signal to have completed is to "hide"
+void FixedObject::signal_send() {
+    physical = false;
+    hidden = true;
+}
+
 void Polygon::draw(Canvas* canvas) {
     if(!hidden) {
         for(size_t i = 0; i < points.size() - 1; i++) {
@@ -208,48 +220,9 @@ bool Polygon::collide(Ball& ball) {
     return true;
 }
 
-// Works-ish - 11/5/2024
-// bool Polygon::collide(Ball& ball) {
-//     Vec2 ball_v = ball.p - ball.prev_p;
-//     // We need to check for collisions across all line segments
-//     for(size_t i = 0; i < points.size() - 1; i++) {
-//         // If ball is moving away from the line, we can't have a collision!
-//         if(normals[i].dot(ball_v) > 0) {
-//             continue;
-//         }
-
-//         Vec2& p1 = points[i];
-//         Vec2& p2 = points[i + 1];
-//         // bool isLeft_prev = Vec2_ccw(p1, p2, ball.prev_p);
-//         // bool isLeft = Vec2_ccw(p1, p2, ball.p);
-//         Vec2 closest = Vec2_closest(p1, p2, ball.p);
-//         float dist = ball.p.dist(closest);
-
-//         if(dist < ball.r) {
-//             // FURI_LOG_I(TAG, "... within collision distance!");
-//             // ball_v.dot
-
-//             // float factor = (ball.r - dist) / ball.r;
-//             // ball.p -= normals[i] * factor;
-//             float depth = ball.r - dist;
-//             ball.p -= normals[i] * depth * 1.05f;
-
-//             Vec2 rel_v = ball_v * -1;
-//             float velAlongNormal = rel_v.dot(normals[i]);
-//             float j = (-(1 + 1) * velAlongNormal);
-//             Vec2 impulse = j * normals[i];
-//             ball_v -= impulse;
-
-//             ball.prev_p = ball.p - ball_v;
-//             return true;
-//         }
-//     }
-//     return false;
-// }
-
 void Polygon::finalize() {
     if(points.size() < 2) {
-        FURI_LOG_E(TAG, "Polygon: FINALIZE_ERROR - insufficient points");
+        FURI_LOG_E(TAG, "Polygon: FINALIZE ERROR - insufficient points");
         return;
     }
     // compute and store normals on all segments
@@ -405,9 +378,16 @@ Arc::Arc(const Vec2& p_, float r_, float s_, float e_, Surface surf_)
     , start(s_)
     , end(e_)
     , surface(surf_) {
+    // Vec2 s(p.x + r * cosf(start), p.y - r * sinf(start));
+    // Vec2 e(p.x + r * cosf(end), p.y - r * sinf(end));
+    // FURI_LOG_I(
+    //     TAG, "ARC: %.2f,%.2f - %.2f,%.2f", (double)s.x, (double)s.y, (double)e.x, (double)e.y);
 }
 
 void Arc::draw(Canvas* canvas) {
+    if(hidden) {
+        return;
+    }
     if(start == 0 && end == (float)M_PI * 2) {
         gfx_draw_circle(canvas, p, r);
     } else {
@@ -552,14 +532,27 @@ void Rollover::draw(Canvas* canvas) {
 }
 
 bool Rollover::collide(Ball& ball) {
+    if(activated) {
+        return false; // we've already rolled over it, prevent further signals
+    }
     Vec2 dir = ball.p - p;
     float dist = dir.mag();
     if(dist < 30) {
         activated = true;
+        return true;
     }
     return false;
 }
 
+// Reset the rollover
+void Rollover::signal_receive() {
+    activated = false;
+}
+
+void Rollover::signal_send() {
+    // maybe we should start a blink animation of the letters?
+}
+
 void Turbo::draw(Canvas* canvas) {
     gfx_draw_line(canvas, chevron_1[0], chevron_1[1]);
     gfx_draw_line(canvas, chevron_1[1], chevron_1[2]);

+ 15 - 5
objects.h

@@ -3,6 +3,8 @@
 #include "vec2.h"
 #include <gui/canvas.h> // for Canvas*
 
+#include "signals.h"
+
 #define DEF_BALL_RADIUS   20
 #define DEF_BUMPER_RADIUS 40
 #define DEF_BUMPER_BOUNCE 1.0f
@@ -92,14 +94,20 @@ public:
         , physical(true)
         , hidden(false)
         , score(0)
+        , tx_id(INVALID_ID)
+        , rx_id(INVALID_ID)
+        , tx_type(SignalType::ALL)
         , notification(nullptr) {
     }
     virtual ~FixedObject() = default;
 
     float bounce;
-    bool physical; // can be hit
+    bool physical; // interacts with ball vs table decoration
     bool hidden; // do not draw
     int score;
+    int tx_id;
+    int rx_id;
+    SignalType tx_type;
 
     void (*notification)(void* app);
 
@@ -107,6 +115,9 @@ public:
     virtual bool collide(Ball& ball) = 0;
     virtual void reset_animation() {};
     virtual void step_animation() {};
+
+    virtual void signal_receive();
+    virtual void signal_send();
 };
 
 class Polygon : public FixedObject {
@@ -213,6 +224,9 @@ public:
 
     void draw(Canvas* canvas);
     bool collide(Ball& ball);
+
+    void signal_receive();
+    void signal_send();
 };
 
 class Turbo : public FixedObject {
@@ -294,7 +308,3 @@ public:
     void draw(Canvas* canvas);
     void step_animation();
 };
-
-// class IconImage : public Object {
-//     Vec2 v;
-// };

+ 3 - 0
pinball0.cxx

@@ -85,6 +85,9 @@ void solve(PinballApp* pb, float dt) {
                     if(o->notification) {
                         (*o->notification)(pb);
                     }
+                    // Send this object's signal (if defined)
+                    table->sm.send(o);
+
                     table->score.value += o->score;
                     o->reset_animation();
                     continue;

+ 118 - 0
signals.cxx

@@ -0,0 +1,118 @@
+#include <furi.h>
+#include "objects.h"
+#include "signals.h"
+
+void SignalManager::register_signal(int id, void* ctx) {
+    // FURI_LOG_I("SIGNAL", "Registered signal, id = %d", id);
+    signals.push_back({id, ctx, false});
+}
+
+void SignalManager::register_slot(int id, void* ctx) {
+    // FURI_LOG_I("SIGNAL", "Registered slot, id = %d", id);
+    slots.push_back({id, ctx, false});
+}
+
+// Send signal 'id' and account for type ALL and ANY
+void SignalManager::send(void* ctx) {
+    FixedObject* obj = (FixedObject*)ctx;
+    int id = obj->tx_id;
+
+    if(id == INVALID_ID) {
+        return;
+    }
+    // FURI_LOG_I("SIGNAL", "Attempting to send signal %d", id);
+    bool signal = true;
+    for(auto& s : signals) {
+        if(s.id == id) {
+            if(s.ctx == ctx) {
+                s.triggered = true;
+                if(((FixedObject*)(ctx))->tx_type == SignalType::ANY) {
+                    signal = true;
+                    break;
+                }
+            }
+            signal = signal & s.triggered;
+        }
+    }
+    if(signal) {
+        // Send the signal to all objects who want it
+        // FURI_LOG_I(
+        //     "SIGNAL",
+        //     "Signals for id=%d, and type=%s",
+        //     id,
+        //     ((FixedObject*)(ctx))->tx_type == SignalType::ALL ? "ALL" : "ANY");
+        for(auto& s : slots) {
+            if(s.id == id) {
+                ((FixedObject*)(s.ctx))->signal_receive();
+            }
+        }
+        // Clear our internal state of what triggered (used for type ALL)
+        reset(id);
+        // Let the signal initiating objects know that we have sent the signal
+        for(auto& s : signals) {
+            if(s.id == id) {
+                ((FixedObject*)(s.ctx))->signal_send();
+            }
+        }
+    }
+}
+
+void SignalManager::reset(int id) {
+    for(auto& s : signals) {
+        if(s.id == id) {
+            s.triggered = false;
+        }
+    }
+}
+
+// better data structures would make this function more efficient
+bool SignalManager::validate(char* err, std::size_t err_size) {
+    // Verify that there is at least one slot for every signal
+    for(const auto& signal : signals) {
+        bool found = false;
+        for(const auto& slot : slots) {
+            if(signal.id == slot.id) {
+                found = true;
+                break;
+            }
+        }
+        if(!found) {
+            FURI_LOG_E("PB0 SIGNAL", "Signal %d has no slots!", signal.id);
+            snprintf(err, err_size, "Signal %d\nhas no\nslots!", signal.id);
+            return false;
+        }
+    }
+    // Verify that there is at least one signal for every slot
+    for(const auto& slot : slots) {
+        bool found = false;
+        for(const auto& signal : signals) {
+            if(slot.id == signal.id) {
+                found = true;
+                break;
+            }
+        }
+        if(!found) {
+            FURI_LOG_E("PB0 SIGNAL", "Slot %d has no signals!", slot.id);
+            snprintf(err, err_size, "Slot %d\nhas no\nsignals!", slot.id);
+            return false;
+        }
+    }
+    // Verify that all objects with the same signal id have the same trigger type
+    bool valid_types = true;
+    for(const auto& signal : signals) {
+        const SignalType signal_type = ((FixedObject*)signal.ctx)->tx_type;
+        for(const auto& s : signals) {
+            FixedObject* s_obj = (FixedObject*)signal.ctx;
+            if(signal.id == s.id && signal_type != s_obj->tx_type) {
+                valid_types = false;
+                FURI_LOG_E("PB0 SIGNAL", "Signal %d has differing type!", s.id);
+                snprintf(err, err_size, "Signal %d\nhas diff\ntype!", s.id);
+                break;
+            }
+        }
+        if(!valid_types) {
+            break;
+        }
+    }
+    return valid_types;
+}

+ 32 - 0
signals.h

@@ -0,0 +1,32 @@
+#pragma once
+#include <vector>
+
+#define INVALID_ID -1
+
+typedef enum {
+    ALL,
+    ANY
+} SignalType;
+
+typedef struct SignalData {
+    int id;
+    void* ctx;
+    bool triggered;
+} SignalData;
+
+class SignalManager {
+public:
+    SignalManager() = default;
+
+    void register_signal(int id, void* ctx);
+    void register_slot(int id, void* ctx);
+
+    void send(void* ctx);
+    void reset(int id);
+
+    bool validate(char* err, std::size_t err_size);
+
+    // all id + ctx pairs must have triggered before we send
+    std::vector<SignalData> signals;
+    std::vector<SignalData> slots;
+};

+ 3 - 0
table.h

@@ -4,6 +4,7 @@
 #include <vector>
 #include "pinball0.h"
 #include "objects.h"
+#include "signals.h"
 
 #define TABLE_SELECT       0
 #define TABLE_ERROR        1
@@ -75,6 +76,8 @@ public:
     uint32_t last_bump;
     uint32_t bump_count;
 
+    SignalManager sm;
+
     void draw(Canvas* canvas);
 };
 

+ 45 - 2
table_parser.cxx

@@ -143,6 +143,25 @@ bool table_file_parse_float(const nx_json* json, const char* key, float& v) {
     return true;
 }
 
+void table_file_parse_signal(const nx_json* json, Table* table, FixedObject* obj) {
+    const nx_json* signal = nx_json_get(json, "signal");
+    if(signal) {
+        int tx = INVALID_ID;
+        int rx = INVALID_ID;
+        if(table_file_parse_int(signal, "tx", tx) && tx != INVALID_ID) {
+            obj->tx_id = tx;
+            table->sm.register_signal(tx, obj);
+        }
+        if(table_file_parse_int(signal, "rx", rx) && rx != INVALID_ID) {
+            obj->rx_id = rx;
+            table->sm.register_slot(rx, obj);
+        }
+        bool any = false;
+        table_file_parse_bool(signal, "any", any);
+        obj->tx_type = any ? SignalType::ANY : SignalType::ALL;
+    }
+}
+
 Table* table_load_table_from_file(PinballApp* pb, size_t index) {
     auto& tmi = pb->table_list.menu_items[index];
 
@@ -165,10 +184,14 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
         storage_file_free(file);
         return NULL;
     }
-    FURI_LOG_I(TAG, "File size is ok!");
     bool ok =
         storage_file_open(file, furi_string_get_cstr(tmi.filename), FSAM_READ, FSOM_OPEN_EXISTING);
-    FURI_LOG_I(TAG, "File opened? %s", ok ? "YES" : "NO");
+    if(!ok) {
+        FURI_LOG_E(TAG, "Failed to open table file: %s", furi_string_get_cstr(tmi.filename));
+        snprintf(pb->text, 256, "Failed\nto open\nfile!");
+        storage_file_free(file);
+        return NULL;
+    }
 
     // read the file as a string
     uint8_t* buffer;
@@ -342,9 +365,20 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                 float bnc = DEF_BUMPER_BOUNCE;
                 table_file_parse_float(bumper, "bounce", bnc);
 
+                bool physical = true;
+                table_file_parse_bool(bumper, "physical", physical);
+
+                bool hidden = false;
+                table_file_parse_bool(bumper, "hidden", hidden);
+
                 Bumper* new_bumper = new Bumper(p, r);
                 new_bumper->bounce = bnc;
                 new_bumper->notification = notify_bumper_hit;
+                new_bumper->physical = physical;
+                new_bumper->hidden = hidden;
+
+                table_file_parse_signal(bumper, table, new_bumper);
+
                 table->objects.push_back(new_bumper);
             }
         }
@@ -534,6 +568,9 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
                     sym = symbol->text_value[0];
                 }
                 Rollover* new_rollover = new Rollover(p, sym);
+
+                table_file_parse_signal(rollover, table, new_rollover);
+
                 table->objects.push_back(new_rollover);
             }
         }
@@ -573,6 +610,12 @@ Table* table_load_table_from_file(PinballApp* pb, size_t index) {
         break;
     } while(false);
 
+    if(!table->sm.validate(pb->text, 256)) {
+        FURI_LOG_E(TAG, "Signal validation failed!");
+        delete table;
+        table = NULL;
+    }
+
     nx_json_free(json);
     free(json_buffer);