diff --git a/ReadMe.md b/ReadMe.md
index 5f91fba05..779267ad7 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -54,7 +54,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
- SFW Mode
- Jamming Files
- Custom subghz presets
-- Subghz and IR signal replication via gpio | Credits to @ankris812, exact commit lost to time as of rn
+- Subghz and IR signal replication via gpio | Credits to @ankris812
- Honda Keys (CVE-2022-27254)
- NSFW Animations tied to the level system. Read more above
- New API Routes for Locale settings
@@ -65,6 +65,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
[Updated]
- All graphics
+- About 1k files to speed things up a lot
- Folder handling for empty ones (Now indicate they are empty)
- Applications now use the new Locale setting
- Compiler now handles all non-compiled faps during build
@@ -95,8 +96,7 @@ Note: This repo is always updated with OFW & Unleashed. No need to mention all t
Known Bugs:
```txt
-- Name Changer app crashes 50% of the time
-- Some apps dont allow to be set as Favorite
+- Nothing rn. Hopefully that wont change
```
----
diff --git a/applications/plugins/asteroids/app.c b/applications/plugins/asteroids/app.c
new file mode 100644
index 000000000..1a3945fd7
--- /dev/null
+++ b/applications/plugins/asteroids/app.c
@@ -0,0 +1,689 @@
+/* Copyright (C) 2023 Salvatore Sanfilippo -- All Rights Reserved
+ * See the LICENSE file for information about the license. */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef PI
+#define PI 3.14159265358979f
+#endif
+
+#define TAG "Asteroids" // Used for logging
+#define DEBUG_MSG 1
+#define SCREEN_XRES 128
+#define SCREEN_YRES 64
+#define GAME_START_LIVES 3
+
+/* The game uses the OK button both to fire and to accelerate the ship.
+ * This makes it a lot more playable since the finger does not have to
+ * move between two keys. However it is important that the extra time the
+ * player needs to press the button to accelerate instead of just firing
+ * is precisely selected to provide a smooth experience. After a few
+ * attempts, it looks like 70 milliseconds is the right spot. */
+#define SHIP_ACCELERATION_KEYPRESS_TIME 70
+
+/* ============================ Data structures ============================= */
+
+typedef struct Ship {
+ float x, /* Ship x position. */
+ y, /* Ship y position. */
+ vx, /* x velocity. */
+ vy, /* y velocity. */
+ rot; /* Current rotation. 2*PI full rotation. */
+} Ship;
+
+typedef struct Bullet {
+ float x, y, vx, vy; /* Fields like in ship. */
+ uint32_t ttl; /* Time to live, in ticks. */
+} Bullet;
+
+typedef struct Asteroid {
+ float x, y, vx, vy, rot, /* Fields like ship. */
+ rot_speed, /* Angular velocity (rot speed and sense). */
+ size; /* Asteroid size. */
+ uint8_t shape_seed; /* Seed to give random shape. */
+} Asteroid;
+
+#define MAXBUL 10 /* Max bullets on the screen. */
+#define MAXAST 32 /* Max asteroids on the screen. */
+#define SHIP_HIT_ANIMATION_LEN 15
+typedef struct AsteroidsApp {
+ /* GUI */
+ Gui *gui;
+ ViewPort *view_port; /* We just use a raw viewport and we render
+ everything into the low level canvas. */
+ FuriMessageQueue *event_queue; /* Key press events go here. */
+
+ /* Game state. */
+ int running; /* Once false exists the app. */
+ bool gameover; /* Game over status. */
+ uint32_t ticks; /* Game ticks. Increments at each refresh. */
+ uint32_t score; /* Game score. */
+ uint32_t lives; /* Number of lives in the current game. */
+ uint32_t ship_hit; /* When non zero, the ship was hit by an asteroid
+ and we need to show an animation as long as
+ its value is non-zero (and decrease it's value
+ at each tick of animation). */
+
+ /* Ship state. */
+ struct Ship ship;
+
+ /* Bullets state. */
+ struct Bullet bullets[MAXBUL]; /* Each bullet state. */
+ int bullets_num; /* Active bullets. */
+ uint32_t last_bullet_tick; /* Tick the last bullet was fired. */
+
+ /* Asteroids state. */
+ Asteroid asteroids[MAXAST]; /* Each asteroid state. */
+ int asteroids_num; /* Active asteroids. */
+
+ uint32_t pressed[InputKeyMAX]; /* pressed[id] is true if pressed.
+ Each array item contains the time
+ in milliseconds the key was pressed. */
+ bool fire; /* Short press detected: fire a bullet. */
+} AsteroidsApp;
+
+/* ============================== Prototypes ================================ */
+
+// Only functions called before their definition are here.
+
+void restart_game_after_gameover(AsteroidsApp *app);
+uint32_t key_pressed_time(AsteroidsApp *app, InputKey key);
+
+/* ============================ 2D drawing ================================== */
+
+/* This structure represents a polygon of at most POLY_MAX points.
+ * The function draw_poly() is able to render it on the screen, rotated
+ * by the amount specified. */
+#define POLY_MAX 8
+typedef struct Poly {
+ float x[POLY_MAX];
+ float y[POLY_MAX];
+ uint32_t points; /* Number of points actually populated. */
+} Poly;
+
+/* Define the polygons we use. */
+Poly ShipPoly = {
+ {-3, 0, 3},
+ {-3, 6, -3},
+ 3
+};
+
+Poly ShipFirePoly = {
+ {-1.5, 0, 1.5},
+ {-3, -6, -3},
+ 3
+};
+
+/* Rotate the point of the polygon 'poly' and store the new rotated
+ * polygon in 'rot'. The polygon is rotated by an angle 'a', with
+ * center at 0,0. */
+void rotate_poly(Poly *rot, Poly *poly, float a) {
+ /* We want to compute sin(a) and cos(a) only one time
+ * for every point to rotate. It's a slow operation. */
+ float sin_a = (float)sin(a);
+ float cos_a = (float)cos(a);
+ for (uint32_t j = 0; j < poly->points; j++) {
+ rot->x[j] = poly->x[j]*cos_a - poly->y[j]*sin_a;
+ rot->y[j] = poly->y[j]*cos_a + poly->x[j]*sin_a;
+ }
+ rot->points = poly->points;
+}
+
+/* This is an 8 bit LFSR we use to generate a predictable and fast
+ * pseudorandom sequence of numbers, to give a different shape to
+ * each asteroid. */
+void lfsr_next(unsigned char *prev) {
+ unsigned char lsb = *prev & 1;
+ *prev = *prev >> 1;
+ if (lsb == 1) *prev ^= 0b11000111;
+ *prev ^= *prev<<7; /* Mix things a bit more. */
+}
+
+/* Render the polygon 'poly' at x,y, rotated by the specified angle. */
+void draw_poly(Canvas *const canvas, Poly *poly, uint8_t x, uint8_t y, float a)
+{
+ Poly rot;
+ rotate_poly(&rot,poly,a);
+ canvas_set_color(canvas, ColorBlack);
+ for (uint32_t j = 0; j < rot.points; j++) {
+ uint32_t a = j;
+ uint32_t b = j+1;
+ if (b == rot.points) b = 0;
+ canvas_draw_line(canvas,x+rot.x[a],y+rot.y[a],
+ x+rot.x[b],y+rot.y[b]);
+ }
+}
+
+/* A bullet is just a + pixels pattern. A single pixel is not
+ * visible enough. */
+void draw_bullet(Canvas *const canvas, Bullet *b) {
+ canvas_draw_dot(canvas,b->x-1,b->y);
+ canvas_draw_dot(canvas,b->x+1,b->y);
+ canvas_draw_dot(canvas,b->x,b->y);
+ canvas_draw_dot(canvas,b->x,b->y-1);
+ canvas_draw_dot(canvas,b->x,b->y+1);
+}
+
+/* Draw an asteroid. The asteroid shapes is computed on the fly and
+ * is not stored in a permanent shape structure. In order to generate
+ * the shape, we use an initial fixed shape that we resize according
+ * to the asteroid size, perturbed according to the asteroid shape
+ * seed, and finally draw it rotated of the right amount. */
+void draw_asteroid(Canvas *const canvas, Asteroid *ast) {
+ Poly ap;
+
+ /* Start with what is kinda of a circle. Note that this could be
+ * stored into a template and copied here, to avoid computing
+ * sin() / cos(). But the Flipper can handle it without problems. */
+ uint8_t r = ast->shape_seed;
+ for (int j = 0; j < 8; j++) {
+ float a = (PI*2)/8*j;
+
+ /* Before generating the point, to make the shape unique generate
+ * a random factor between .7 and 1.3 to scale the distance from
+ * the center. However this asteroid should have its unique shape
+ * that remains always the same, so we use a predictable PRNG
+ * implemented by an 8 bit shift register. */
+ lfsr_next(&r);
+ float scaling = .7+((float)r/255*.6);
+
+ ap.x[j] = (float)sin(a) * ast->size * scaling;
+ ap.y[j] = (float)cos(a) * ast->size * scaling;
+ }
+ ap.points = 8;
+ draw_poly(canvas,&ap,ast->x,ast->y,ast->rot);
+}
+
+/* Draw small ships in the top-right part of the screen, one for
+ * each left live. */
+void draw_left_lives(Canvas *const canvas, AsteroidsApp *app) {
+ int lives = app->lives;
+ int x = SCREEN_XRES-5;
+
+ Poly mini_ship = {
+ {-2, 0, 2},
+ {-2, 4, -2},
+ 3
+ };
+ while(lives--) {
+ draw_poly(canvas,&mini_ship,x,6,PI);
+ x -= 6;
+ }
+}
+
+/* Given the current position, update it according to the velocity and
+ * wrap it back to the other side if the object went over the screen. */
+void update_pos_by_velocity(float *x, float *y, float vx, float vy) {
+ /* Return back from one side to the other of the screen. */
+ *x += vx;
+ *y += vy;
+ if (*x >= SCREEN_XRES) *x = 0;
+ else if (*x < 0) *x = SCREEN_XRES-1;
+ if (*y >= SCREEN_YRES) *y = 0;
+ else if (*y < 0) *y = SCREEN_YRES-1;
+}
+
+/* Render the current game screen. */
+void render_callback(Canvas *const canvas, void *ctx) {
+ AsteroidsApp *app = ctx;
+
+ /* Clear screen. */
+ canvas_set_color(canvas, ColorWhite);
+ canvas_draw_box(canvas, 0, 0, SCREEN_XRES-1, SCREEN_YRES-1);
+
+ /* Draw score. */
+ canvas_set_color(canvas, ColorBlack);
+ canvas_set_font(canvas, FontSecondary);
+ char score[32];
+ snprintf(score,sizeof(score),"%lu",app->score);
+ canvas_draw_str(canvas, 0, 8, score);
+
+ /* Draw left ships. */
+ draw_left_lives(canvas,app);
+
+ /* Draw ship, asteroids, bullets. */
+ draw_poly(canvas,&ShipPoly,app->ship.x,app->ship.y,app->ship.rot);
+ if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME)
+ draw_poly(canvas,&ShipFirePoly,app->ship.x,app->ship.y,app->ship.rot);
+
+ for (int j = 0; j < app->bullets_num; j++)
+ draw_bullet(canvas,&app->bullets[j]);
+
+ for (int j = 0; j < app->asteroids_num; j++)
+ draw_asteroid(canvas,&app->asteroids[j]);
+
+ /* Game over text. */
+ if (app->gameover) {
+ canvas_set_color(canvas, ColorBlack);
+ canvas_set_font(canvas, FontPrimary);
+ canvas_draw_str(canvas, 28, 35, "GAME OVER");
+ canvas_set_font(canvas, FontSecondary);
+ canvas_draw_str(canvas, 25, 50, "Press OK to restart");
+ }
+}
+
+/* ============================ Game logic ================================== */
+
+float distance(float x1, float y1, float x2, float y2) {
+ float dx = x1-x2;
+ float dy = y1-y2;
+ return sqrt(dx*dx+dy*dy);
+}
+
+/* Detect a collision between the object at x1,y1 of radius r1 and
+ * the object at x2, y2 of radius r2. A factor < 1 will make the
+ * function detect the collision even if the objects are yet not
+ * relly touching, while a factor > 1 will make it detect the collision
+ * only after they are a bit overlapping. It basically is used to
+ * rescale the distance.
+ *
+ * Note that in this simplified 2D world, objects are all considered
+ * spheres (this is why this function only takes the radius). This
+ * is, after all, kinda accurate for asteroids, for bullets, and
+ * even for the ship "core" itself. */
+bool objects_are_colliding(float x1, float y1, float r1,
+ float x2, float y2, float r2,
+ float factor)
+{
+ /* The objects are colliding if the distance between object 1 and 2
+ * is smaller than the sum of the two radiuses r1 and r2.
+ * So it would be like: sqrt((x1-x2)^2+(y1-y2)^2) < r1+r2.
+ * However we can avoid computing the sqrt (which is slow) by
+ * squaring the second term and removing the square root, making
+ * the comparison like this:
+ *
+ * (x1-x2)^2+(y1-y2)^2 < (r1+r2)^2. */
+ float dx = (x1-x2)*factor;
+ float dy = (y1-y2)*factor;
+ float rsum = r1+r2;
+ return dx*dx+dy*dy < rsum*rsum;
+}
+
+/* Create a new bullet headed in the same direction of the ship. */
+void ship_fire_bullet(AsteroidsApp *app) {
+ if (app->bullets_num == MAXBUL) return;
+ Bullet *b = &app->bullets[app->bullets_num];
+ b->x = app->ship.x;
+ b->y = app->ship.y;
+ b->vx = -sin(app->ship.rot);
+ b->vy = cos(app->ship.rot);
+
+ /* Ship should fire from its head, not in the middle. */
+ b->x += b->vx*5;
+ b->y += b->vy*5;
+
+ /* Give the bullet some velocity (for now the vector is just
+ * normalized to 1). */
+ b->vx *= 3;
+ b->vy *= 3;
+
+ /* It's more realistic if we add the velocity vector of the
+ * ship, too. Otherwise if the ship is going fast the bullets
+ * will be slower, which is not how the world works. */
+ b->vx += app->ship.vx;
+ b->vy += app->ship.vy;
+
+ b->ttl = 50; /* The bullet will disappear after N ticks. */
+ app->bullets_num++;
+}
+
+/* Remove the specified bullet by id (index in the array). */
+void remove_bullet(AsteroidsApp *app, int bid) {
+ /* Replace the top bullet with the empty space left
+ * by the removal of this bullet. This way we always take the
+ * array dense, which is an advantage when looping. */
+ int n = --app->bullets_num;
+ if (n && bid != n) app->bullets[bid] = app->bullets[n];
+}
+
+/* Create a new asteroid, away from the ship. Return the
+ * pointer to the asteroid object, so that the caller can change
+ * certain things of the asteroid if needed. */
+Asteroid *add_asteroid(AsteroidsApp *app) {
+ if (app->asteroids_num == MAXAST) return NULL;
+ float size = 4+rand()%15;
+ float min_distance = 20;
+ float x,y;
+ do {
+ x = rand() % SCREEN_XRES;
+ y = rand() % SCREEN_YRES;
+ } while(distance(app->ship.x,app->ship.y,x,y) < min_distance+size);
+ Asteroid *a = &app->asteroids[app->asteroids_num++];
+ a->x = x;
+ a->y = y;
+ a->vx = 2*(-.5 + ((float)rand()/RAND_MAX));
+ a->vy = 2*(-.5 + ((float)rand()/RAND_MAX));
+ a->size = size;
+ a->rot = 0;
+ a->rot_speed = ((float)rand()/RAND_MAX)/10;
+ if (app->ticks & 1) a->rot_speed = -(a->rot_speed);
+ a->shape_seed = rand() & 255;
+ return a;
+}
+
+/* Remove the specified asteroid by id (index in the array). */
+void remove_asteroid(AsteroidsApp *app, int id) {
+ /* Replace the top asteroid with the empty space left
+ * by the removal of this one. This way we always take the
+ * array dense, which is an advantage when looping. */
+ int n = --app->asteroids_num;
+ if (n && id != n) app->asteroids[id] = app->asteroids[n];
+}
+
+/* Called when an asteroid was reached by a bullet. The asteroid
+ * hit is the one with the specified 'id'. */
+void asteroid_was_hit(AsteroidsApp *app, int id) {
+ float sizelimit = 6; // Smaller than that, they disappear in one shot.
+ Asteroid *a = &app->asteroids[id];
+
+ /* Asteroid is large enough to break into fragments. */
+ float size = a->size;
+ float x = a->x, y = a->y;
+ remove_asteroid(app,id);
+ if (size > sizelimit) {
+ int max_fragments = size / sizelimit;
+ int fragments = 2+rand()%max_fragments;
+ float newsize = size/fragments;
+ if (newsize < 2) newsize = 2;
+ for (int j = 0; j < fragments; j++) {
+ a = add_asteroid(app);
+ if (a == NULL) break; // Too many asteroids on screen.
+ a->x = x + -(size/2) + rand() % (int)newsize;
+ a->y = y + -(size/2) + rand() % (int)newsize;
+ a->size = newsize;
+ }
+ } else {
+ app->score++;
+ }
+}
+
+/* Set game over state. When in game-over mode, the game displays a
+ * game over text with a background of many asteroids floating around. */
+void game_over(AsteroidsApp *app) {
+ restart_game_after_gameover(app);
+ app->gameover = true;
+ int asteroids = 8;
+ while(asteroids-- && add_asteroid(app) != NULL);
+}
+
+/* Function called when a collision between the asteroid and the
+ * ship is detected. */
+void ship_was_hit(AsteroidsApp *app) {
+ app->ship_hit = SHIP_HIT_ANIMATION_LEN;
+ if (app->lives) {
+ app->lives--;
+ } else {
+ game_over(app);
+ }
+}
+
+/* Restart game after the ship is hit. Will reset the ship position, bullets
+ * and asteroids to restart the game. */
+void restart_game(AsteroidsApp *app) {
+ app->ship.x = SCREEN_XRES / 2;
+ app->ship.y = SCREEN_YRES / 2;
+ app->ship.rot = PI; /* Start headed towards top. */
+ app->ship.vx = 0;
+ app->ship.vy = 0;
+ app->bullets_num = 0;
+ app->last_bullet_tick = 0;
+ app->asteroids_num = 0;
+}
+
+/* Called after game over to restart the game. This function
+ * also calls restart_game(). */
+void restart_game_after_gameover(AsteroidsApp *app) {
+ app->gameover = false;
+ app->ticks = 0;
+ app->score = 0;
+ app->ship_hit = 0;
+ app->lives = GAME_START_LIVES-1; /* -1 to account for current one. */
+ restart_game(app);
+}
+
+/* Move bullets. */
+void update_bullets_position(AsteroidsApp *app) {
+ for (int j = 0; j < app->bullets_num; j++) {
+ update_pos_by_velocity(&app->bullets[j].x,&app->bullets[j].y,
+ app->bullets[j].vx,app->bullets[j].vy);
+ if (--app->bullets[j].ttl == 0) {
+ remove_bullet(app,j);
+ j--; /* Process this bullet index again: the removal will
+ fill it with the top bullet to take the array dense. */
+ }
+ }
+}
+
+/* Move asteroids. */
+void update_asteroids_position(AsteroidsApp *app) {
+ for (int j = 0; j < app->asteroids_num; j++) {
+ update_pos_by_velocity(&app->asteroids[j].x,&app->asteroids[j].y,
+ app->asteroids[j].vx,app->asteroids[j].vy);
+ app->asteroids[j].rot += app->asteroids[j].rot_speed;
+ if (app->asteroids[j].rot < 0) app->asteroids[j].rot = 2*PI;
+ else if (app->asteroids[j].rot > 2*PI) app->asteroids[j].rot = 0;
+ }
+}
+
+/* Collision detection and game state update based on collisions. */
+void detect_collisions(AsteroidsApp *app) {
+ /* Detect collision between bullet and asteroid. */
+ for (int j = 0; j < app->bullets_num; j++) {
+ Bullet *b = &app->bullets[j];
+ for (int i = 0; i < app->asteroids_num; i++) {
+ Asteroid *a = &app->asteroids[i];
+ if (objects_are_colliding(a->x, a->y, a->size,
+ b->x, b->y, 1.5, 1))
+ {
+ asteroid_was_hit(app,i);
+ remove_bullet(app,j);
+ /* The bullet no longer exist. Break the loop.
+ * However we want to start processing from the
+ * same bullet index, since now it is used by
+ * another bullet (see remove_bullet()). */
+ j--; /* Scan this j value again. */
+ break;
+ }
+ }
+ }
+
+ /* Detect collision between ship and asteroid. */
+ for (int j = 0; j < app->asteroids_num; j++) {
+ Asteroid *a = &app->asteroids[j];
+ if (objects_are_colliding(a->x, a->y, a->size,
+ app->ship.x, app->ship.y, 4, 1))
+ {
+ ship_was_hit(app);
+ break;
+ }
+ }
+}
+
+/* This is the main game execution function, called 10 times for
+ * second (with the Flipper screen latency, an higher FPS does not
+ * make sense). In this function we update the position of objects based
+ * on velocity. Detect collisions. Update the score and so forth.
+ *
+ * Each time this function is called, app->tick is incremented. */
+void game_tick(void *ctx) {
+ AsteroidsApp *app = ctx;
+
+ /* There are two special screens:
+ *
+ * 1. Ship was hit, we frozen the game as long as ship_hit isn't zero
+ * again, and show an animation of a rotating ship. */
+ if (app->ship_hit) {
+ app->ship.rot += 0.5;
+ app->ship_hit--;
+ view_port_update(app->view_port);
+ if (app->ship_hit == 0) {
+ restart_game(app);
+ }
+ return;
+ } else if (app->gameover) {
+ /* 2. Game over. We need to update only background asteroids. In this
+ * state the game just displays a GAME OVER text with the floating
+ * asteroids in background. */
+ if (key_pressed_time(app,InputKeyOk) > 100) {
+ restart_game_after_gameover(app);
+ }
+ update_asteroids_position(app);
+ view_port_update(app->view_port);
+ return;
+ }
+
+ /* Handle key presses. */
+ if (app->pressed[InputKeyLeft]) app->ship.rot -= .35;
+ if (app->pressed[InputKeyRight]) app->ship.rot += .35;
+ if (key_pressed_time(app,InputKeyOk) > SHIP_ACCELERATION_KEYPRESS_TIME) {
+ app->ship.vx -= 0.5*(float)sin(app->ship.rot);
+ app->ship.vy += 0.5*(float)cos(app->ship.rot);
+ } else if (app->pressed[InputKeyDown]) {
+ app->ship.vx *= 0.75;
+ app->ship.vy *= 0.75;
+ }
+
+ /* Fire a bullet if needed. app->fire is set in
+ * asteroids_update_keypress_state() since depends on exact
+ * pressure timing. */
+ if (app->fire) {
+ uint32_t bullet_min_period = 200; // In milliseconds
+ uint32_t now = furi_get_tick();
+ if (now - app->last_bullet_tick >= bullet_min_period) {
+ ship_fire_bullet(app);
+ app->last_bullet_tick = now;
+ }
+ app->fire = false;
+ }
+
+ /* Update positions and detect collisions. */
+ update_pos_by_velocity(&app->ship.x,&app->ship.y,app->ship.vx,app->ship.vy);
+ update_bullets_position(app);
+ update_asteroids_position(app);
+ detect_collisions(app);
+
+ /* From time to time, create a new asteroid. The more asteroids
+ * already on the screen, the smaller probability of creating
+ * a new one. */
+ if (app->asteroids_num == 0 ||
+ (random() % 5000) < (30/(1+app->asteroids_num)))
+ {
+ add_asteroid(app);
+ }
+
+ app->ticks++;
+ view_port_update(app->view_port);
+}
+
+/* ======================== Flipper specific code =========================== */
+
+/* Here all we do is putting the events into the queue that will be handled
+ * in the while() loop of the app entry point function. */
+void input_callback(InputEvent* input_event, void* ctx)
+{
+ AsteroidsApp *app = ctx;
+ furi_message_queue_put(app->event_queue,input_event,FuriWaitForever);
+}
+
+/* Allocate the application state and initialize a number of stuff.
+ * This is called in the entry point to create the application state. */
+AsteroidsApp* asteroids_app_alloc() {
+ AsteroidsApp *app = malloc(sizeof(AsteroidsApp));
+
+ app->gui = furi_record_open(RECORD_GUI);
+ app->view_port = view_port_alloc();
+ view_port_draw_callback_set(app->view_port, render_callback, app);
+ view_port_input_callback_set(app->view_port, input_callback, app);
+ gui_add_view_port(app->gui, app->view_port, GuiLayerFullscreen);
+ app->event_queue = furi_message_queue_alloc(8, sizeof(InputEvent));
+
+ app->running = 1; /* Turns 0 when back is pressed. */
+ restart_game_after_gameover(app);
+ memset(app->pressed,0,sizeof(app->pressed));
+ return app;
+}
+
+/* Free what the application allocated. It is not clear to me if the
+ * Flipper OS, once the application exits, will be able to reclaim space
+ * even if we forget to free something here. */
+void asteroids_app_free(AsteroidsApp *app) {
+ furi_assert(app);
+
+ // View related.
+ view_port_enabled_set(app->view_port, false);
+ gui_remove_view_port(app->gui, app->view_port);
+ view_port_free(app->view_port);
+ furi_record_close(RECORD_GUI);
+ furi_message_queue_free(app->event_queue);
+ app->gui = NULL;
+
+ free(app);
+}
+
+/* Return the time in milliseconds the specified key is continuously
+ * pressed. Or 0 if it is not pressed. */
+uint32_t key_pressed_time(AsteroidsApp *app, InputKey key) {
+ return app->pressed[key] == 0 ? 0 :
+ furi_get_tick() - app->pressed[key];
+}
+
+/* Handle keys interaction. */
+void asteroids_update_keypress_state(AsteroidsApp *app, InputEvent input) {
+ if (input.type == InputTypePress) {
+ app->pressed[input.key] = furi_get_tick();
+ } else if (input.type == InputTypeRelease) {
+ uint32_t dur = key_pressed_time(app,input.key);
+ app->pressed[input.key] = 0;
+ if (dur < 200 && input.key == InputKeyOk) app->fire = true;
+ }
+}
+
+int32_t asteroids_app_entry(void* p) {
+ UNUSED(p);
+ AsteroidsApp *app = asteroids_app_alloc();
+
+ /* Create a timer. We do data analysis in the callback. */
+ FuriTimer *timer = furi_timer_alloc(game_tick, FuriTimerTypePeriodic, app);
+ furi_timer_start(timer, furi_kernel_get_tick_frequency() / 10);
+
+ /* This is the main event loop: here we get the events that are pushed
+ * in the queue by input_callback(), and process them one after the
+ * other. */
+ InputEvent input;
+ while(app->running) {
+ FuriStatus qstat = furi_message_queue_get(app->event_queue, &input, 100);
+ if (qstat == FuriStatusOk) {
+ if (DEBUG_MSG) FURI_LOG_E(TAG, "Main Loop - Input: type %d key %u",
+ input.type, input.key);
+
+ /* Handle navigation here. Then handle view-specific inputs
+ * in the view specific handling function. */
+ if (input.type == InputTypeShort &&
+ input.key == InputKeyBack)
+ {
+ app->running = 0;
+ } else {
+ asteroids_update_keypress_state(app,input);
+ }
+ } else {
+ /* Useful to understand if the app is still alive when it
+ * does not respond because of bugs. */
+ if (DEBUG_MSG) {
+ static int c = 0; c++;
+ if (!(c % 20)) FURI_LOG_E(TAG, "Loop timeout");
+ }
+ }
+ }
+
+ furi_timer_free(timer);
+ asteroids_app_free(app);
+ return 0;
+}
diff --git a/applications/plugins/asteroids/appicon.png b/applications/plugins/asteroids/appicon.png
new file mode 100644
index 000000000..45da095af
Binary files /dev/null and b/applications/plugins/asteroids/appicon.png differ
diff --git a/applications/plugins/asteroids/application.fam b/applications/plugins/asteroids/application.fam
new file mode 100644
index 000000000..0a56122e7
--- /dev/null
+++ b/applications/plugins/asteroids/application.fam
@@ -0,0 +1,12 @@
+App(
+ appid="asteroids",
+ name="Asteroids",
+ apptype=FlipperAppType.EXTERNAL,
+ entry_point="asteroids_app_entry",
+ cdefines=["APP_PROTOVIEW"],
+ requires=["gui"],
+ stack_size=8*1024,
+ order=50,
+ fap_icon="appicon.png",
+ fap_category="Games",
+)
diff --git a/applications/plugins/pong/application.fam b/applications/plugins/pong/application.fam
new file mode 100644
index 000000000..95484b6e7
--- /dev/null
+++ b/applications/plugins/pong/application.fam
@@ -0,0 +1,13 @@
+App(
+ appid="flipper_pong",
+ name="Pong",
+ apptype=FlipperAppType.EXTERNAL,
+ entry_point="flipper_pong_app",
+ cdefines=["APP_FLIPPER_PONG"],
+ requires=[
+ "gui",
+ ],
+ stack_size=1 * 1024,
+ fap_icon="pong.png",
+ fap_category="Games",
+)
diff --git a/applications/plugins/pong/flipper_pong.c b/applications/plugins/pong/flipper_pong.c
new file mode 100644
index 000000000..c25e46b7b
--- /dev/null
+++ b/applications/plugins/pong/flipper_pong.c
@@ -0,0 +1,298 @@
+// CC0 1.0 Universal (CC0 1.0)
+// Public Domain Dedication
+// https://github.com/nmrr
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define SCREEN_SIZE_X 128
+#define SCREEN_SIZE_Y 64
+#define FPS 20
+
+#define PAD_SIZE_X 3
+#define PAD_SIZE_Y 8
+#define PLAYER1_PAD_SPEED 2
+#define PLAYER2_PAD_SPEED 2
+#define BALL_SIZE 4
+
+typedef enum {
+ EventTypeInput,
+ ClockEventTypeTick,
+} EventType;
+
+typedef struct {
+ EventType type;
+ InputEvent input;
+} EventApp;
+
+typedef struct Players
+{
+ uint8_t player1_X,player1_Y,player2_X,player2_Y;
+ uint16_t player1_score,player2_score;
+ uint8_t ball_X,ball_Y,ball_X_speed,ball_Y_speed,ball_X_direction,ball_Y_direction;
+} Players;
+
+static void draw_callback(Canvas* canvas, void* ctx)
+{
+ UNUSED(ctx);
+ Players* playersMutex = (Players*)acquire_mutex_block((ValueMutex*)ctx);
+
+ canvas_draw_frame(canvas, 0, 0, 128, 64);
+ canvas_draw_box(canvas, playersMutex->player1_X, playersMutex->player1_Y, PAD_SIZE_X, PAD_SIZE_Y);
+ canvas_draw_box(canvas, playersMutex->player2_X, playersMutex->player2_Y, PAD_SIZE_X, PAD_SIZE_Y);
+ canvas_draw_box(canvas, playersMutex->ball_X, playersMutex->ball_Y, BALL_SIZE, BALL_SIZE);
+
+ canvas_set_font(canvas, FontPrimary);
+ canvas_set_font_direction(canvas, CanvasDirectionBottomToTop);
+ char buffer[16];
+ snprintf(buffer, sizeof(buffer), "%u - %u", playersMutex->player1_score, playersMutex->player2_score);
+ canvas_draw_str_aligned(canvas, SCREEN_SIZE_X/2+15, SCREEN_SIZE_Y/2+2, AlignCenter, AlignTop, buffer);
+
+ release_mutex((ValueMutex*)ctx, playersMutex);
+}
+
+static void input_callback(InputEvent* input_event, void* ctx)
+{
+ furi_assert(ctx);
+ FuriMessageQueue* event_queue = ctx;
+ EventApp event = {.type = EventTypeInput, .input = *input_event};
+ furi_message_queue_put(event_queue, &event, FuriWaitForever);
+}
+
+static void clock_tick(void* ctx) {
+ furi_assert(ctx);
+ FuriMessageQueue* queue = ctx;
+ EventApp event = {.type = ClockEventTypeTick};
+ furi_message_queue_put(queue, &event, 0);
+}
+
+bool insidePad(uint8_t x, uint8_t y, uint8_t playerX, uint8_t playerY)
+{
+ if (x >= playerX && x <= playerX+PAD_SIZE_X && y >= playerY && y <= playerY+PAD_SIZE_Y) return true;
+ return false;
+}
+
+uint8_t changeSpeed()
+{
+ uint8_t randomuint8[1];
+ while(1)
+ {
+ furi_hal_random_fill_buf(randomuint8,1);
+ randomuint8[0] &= 0b00000011;
+ if (randomuint8[0] >= 1) break;
+ }
+ return randomuint8[0];
+}
+
+uint8_t changeDirection()
+{
+ uint8_t randomuint8[1];
+ furi_hal_random_fill_buf(randomuint8,1);
+ randomuint8[0] &= 0b1;
+ return randomuint8[0];
+}
+
+int32_t flipper_pong_app()
+{
+ EventApp event;
+ FuriMessageQueue* event_queue = furi_message_queue_alloc(8, sizeof(EventApp));
+
+ Players players;
+ players.player1_X = SCREEN_SIZE_X-PAD_SIZE_X-1;
+ players.player1_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2;
+ players.player1_score = 0;
+
+ players.player2_X = 1;
+ players.player2_Y = SCREEN_SIZE_Y/2 - PAD_SIZE_Y/2;
+ players.player2_score = 0;
+
+ players.ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
+ players.ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
+ players.ball_X_speed = 1;
+ players.ball_Y_speed = 1;
+ players.ball_X_direction = changeDirection();
+ players.ball_Y_direction = changeDirection();
+
+ ValueMutex state_mutex;
+ init_mutex(&state_mutex, &players, sizeof(Players));
+
+ ViewPort* view_port = view_port_alloc();
+ view_port_draw_callback_set(view_port, draw_callback, &state_mutex);
+ view_port_input_callback_set(view_port, input_callback, event_queue);
+
+ Gui* gui = furi_record_open(RECORD_GUI);
+ gui_add_view_port(gui, view_port, GuiLayerFullscreen);
+
+ NotificationApp* notification = furi_record_open(RECORD_NOTIFICATION);
+ if (players.ball_X_direction == 0) notification_message(notification, &sequence_set_only_red_255);
+ else notification_message(notification, &sequence_set_only_blue_255);
+
+ FuriTimer* timer = furi_timer_alloc(clock_tick, FuriTimerTypePeriodic, event_queue);
+ furi_timer_start(timer, 1000/FPS);
+
+ while(1)
+ {
+ FuriStatus event_status = furi_message_queue_get(event_queue, &event, FuriWaitForever);
+ Players* playersMutex = (Players*)acquire_mutex_block(&state_mutex);
+
+ if (event_status == FuriStatusOk)
+ {
+ if(event.type == EventTypeInput)
+ {
+ if(event.input.key == InputKeyBack)
+ {
+ release_mutex(&state_mutex, playersMutex);
+ notification_message(notification, &sequence_set_only_green_255);
+ break;
+ }
+ else if(event.input.key == InputKeyUp)
+ {
+ if (playersMutex->player1_Y >= 1+PLAYER1_PAD_SPEED) playersMutex->player1_Y -= PLAYER1_PAD_SPEED;
+ else playersMutex->player1_Y = 1;
+ }
+ else if(event.input.key == InputKeyDown)
+ {
+ if (playersMutex->player1_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER1_PAD_SPEED -1) playersMutex->player1_Y += PLAYER1_PAD_SPEED;
+ else playersMutex->player1_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1;
+ }
+ }
+ else if (event.type == ClockEventTypeTick)
+ {
+
+ if (playersMutex->ball_X + BALL_SIZE/2 <= SCREEN_SIZE_X*0.35 && playersMutex->ball_X_direction == 0)
+ {
+ if (playersMutex->ball_Y + BALL_SIZE/2 < playersMutex->player2_Y + PAD_SIZE_Y/2)
+ {
+ if (playersMutex->player2_Y >= 1+PLAYER2_PAD_SPEED) playersMutex->player2_Y -= PLAYER2_PAD_SPEED;
+ else playersMutex->player2_Y= 1;
+ }
+ else if (playersMutex->ball_Y + BALL_SIZE/2 > playersMutex->player2_Y + PAD_SIZE_Y/2)
+ {
+ if (playersMutex->player2_Y <= SCREEN_SIZE_Y - PAD_SIZE_Y - PLAYER2_PAD_SPEED -1) playersMutex->player2_Y += PLAYER2_PAD_SPEED;
+ else playersMutex->player2_Y = SCREEN_SIZE_Y - PAD_SIZE_Y - 1;
+ }
+ }
+
+ uint8_t ball_corner_X[4] = {playersMutex->ball_X, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X + BALL_SIZE, playersMutex->ball_X};
+ uint8_t ball_corner_Y[4] = {playersMutex->ball_Y, playersMutex->ball_Y, playersMutex->ball_Y + BALL_SIZE, playersMutex->ball_Y + BALL_SIZE};
+ bool insidePlayer1 = false, insidePlayer2 = false;
+
+ for (int i=0;i<4;i++)
+ {
+ if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player1_X, playersMutex->player1_Y) == true)
+ {
+ insidePlayer1 = true;
+ break;
+ }
+
+ if (insidePad(ball_corner_X[i], ball_corner_Y[i], playersMutex->player2_X, playersMutex->player2_Y) == true)
+ {
+ insidePlayer2 = true;
+ break;
+ }
+ }
+
+ if (insidePlayer1 == true)
+ {
+ playersMutex->ball_X_direction = 0;
+ playersMutex->ball_X -= playersMutex->ball_X_speed;
+ playersMutex->ball_X_speed = changeSpeed();
+ playersMutex->ball_Y_speed = changeSpeed();
+ notification_message(notification, &sequence_set_only_red_255);
+ }
+ else if (insidePlayer2 == true)
+ {
+ playersMutex->ball_X_direction = 1;
+ playersMutex->ball_X += playersMutex->ball_X_speed;
+ playersMutex->ball_X_speed = changeSpeed();
+ playersMutex->ball_Y_speed = changeSpeed();
+ notification_message(notification, &sequence_set_only_blue_255);
+ }
+ else
+ {
+ if (playersMutex->ball_X_direction == 1)
+ {
+
+ if (playersMutex->ball_X <= SCREEN_SIZE_X - BALL_SIZE - 1 - playersMutex->ball_X_speed)
+ {
+ playersMutex->ball_X += playersMutex->ball_X_speed;
+ }
+ else
+ {
+ playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
+ playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
+ playersMutex->ball_X_speed = 1;
+ playersMutex->ball_Y_speed = 1;
+ playersMutex->ball_X_direction = 0;
+ playersMutex->player2_score++;
+ notification_message(notification, &sequence_set_only_red_255);
+ }
+ }
+ else
+ {
+ if (playersMutex->ball_X >= 1 + playersMutex->ball_X_speed)
+ {
+ playersMutex->ball_X -= playersMutex->ball_X_speed;
+ }
+ else
+ {
+ playersMutex->ball_X = SCREEN_SIZE_X/2 - BALL_SIZE/2;
+ playersMutex->ball_Y = SCREEN_SIZE_Y/2 - BALL_SIZE/2;
+ playersMutex->ball_X_speed = 1;
+ playersMutex->ball_Y_speed = 1;
+ playersMutex->ball_X_direction = 1;
+ playersMutex->player1_score++;
+ notification_message(notification, &sequence_set_only_blue_255);
+ }
+ }
+ }
+
+ if (playersMutex->ball_Y_direction == 1)
+ {
+ if (playersMutex->ball_Y <= SCREEN_SIZE_Y - BALL_SIZE - 1 - playersMutex->ball_Y_speed)
+ {
+ playersMutex->ball_Y += playersMutex->ball_Y_speed;
+ }
+ else
+ {
+ playersMutex->ball_Y = SCREEN_SIZE_Y - BALL_SIZE - 1;
+ playersMutex->ball_X_speed = changeSpeed();
+ playersMutex->ball_Y_speed = changeSpeed();
+ playersMutex->ball_Y_direction = 0;
+ }
+ }
+ else
+ {
+ if (playersMutex->ball_Y >= 1 + playersMutex->ball_Y_speed)
+ {
+ playersMutex->ball_Y -= playersMutex->ball_Y_speed;
+ }
+ else
+ {
+ playersMutex->ball_Y = 1;
+ playersMutex->ball_X_speed = changeSpeed();
+ playersMutex->ball_Y_speed = changeSpeed();
+ playersMutex->ball_Y_direction = 1;
+ }
+ }
+ }
+ }
+
+ release_mutex(&state_mutex, playersMutex);
+ view_port_update(view_port);
+ }
+
+ furi_message_queue_free(event_queue);
+ delete_mutex(&state_mutex);
+ gui_remove_view_port(gui, view_port);
+ view_port_free(view_port);
+ furi_timer_free(timer);
+ furi_record_close(RECORD_GUI);
+ furi_record_close(RECORD_NOTIFICATION);
+
+ return 0;
+}
\ No newline at end of file
diff --git a/applications/plugins/pong/pong.png b/applications/plugins/pong/pong.png
new file mode 100644
index 000000000..507ce711c
Binary files /dev/null and b/applications/plugins/pong/pong.png differ
diff --git a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c
index 6e5c72624..9124ce287 100644
--- a/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c
+++ b/applications/settings/desktop_settings/scenes/desktop_settings_scene_start.c
@@ -10,7 +10,6 @@
#define SCENE_EVENT_SELECT_PIN_SETUP 2
#define SCENE_EVENT_SELECT_AUTO_LOCK_DELAY 3
#define SCENE_EVENT_SELECT_BATTERY_DISPLAY 4
-#define SCENE_EVENT_SELECT_SFWMODE 5
#define AUTO_LOCK_DELAY_COUNT 9
const char* const auto_lock_delay_text[AUTO_LOCK_DELAY_COUNT] = {
@@ -39,14 +38,6 @@ const uint32_t displayBatteryPercentage_value[BATTERY_VIEW_COUNT] = {0, 1, 2, 3,
uint8_t origBattDisp_value = 0;
-#define SFWMODE_COUNT 2
-const char* const sfwmode_text[SFWMODE_COUNT] = {
- "OFF",
- "ON",
-};
-
-const uint32_t sfwmode_value[SFWMODE_COUNT] = {0, 1};
-
static void desktop_settings_scene_start_var_list_enter_callback(void* context, uint32_t index) {
DesktopSettingsApp* app = context;
view_dispatcher_send_custom_event(app->view_dispatcher, index);
@@ -68,14 +59,6 @@ static void desktop_settings_scene_start_battery_view_changed(VariableItem* item
app->settings.displayBatteryPercentage = index;
}
-static void desktop_settings_scene_start_sfwmode_changed(VariableItem* item) {
- DesktopSettingsApp* app = variable_item_get_context(item);
- uint8_t index = variable_item_get_current_value_index(item);
-
- variable_item_set_current_value_text(item, sfwmode_text[index]);
- app->settings.is_sfwmode = sfwmode_value[index];
-}
-
void desktop_settings_scene_start_on_enter(void* context) {
DesktopSettingsApp* app = context;
VariableItemList* variable_item_list = app->variable_item_list;
@@ -118,17 +101,6 @@ void desktop_settings_scene_start_on_enter(void* context) {
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, battery_view_count_text[value_index]);
- item = variable_item_list_add(
- variable_item_list,
- "SFW Content Only",
- SFWMODE_COUNT,
- desktop_settings_scene_start_sfwmode_changed,
- app);
-
- value_index = value_index_uint32(app->settings.is_sfwmode, sfwmode_value, SFWMODE_COUNT);
- variable_item_set_current_value_index(item, value_index);
- variable_item_set_current_value_text(item, sfwmode_text[value_index]);
-
variable_item_list_set_enter_callback(
variable_item_list, desktop_settings_scene_start_var_list_enter_callback, app);
view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewVarItemList);
@@ -165,9 +137,6 @@ bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent sme)
case SCENE_EVENT_SELECT_BATTERY_DISPLAY:
consumed = true;
break;
- case SCENE_EVENT_SELECT_SFWMODE:
- consumed = true;
- break;
}
}
return consumed;
diff --git a/assets/resources/subghz/playlist/Lowes_playlist.txt b/assets/resources/subghz/playlist/Lowes_playlist.txt
new file mode 100644
index 000000000..b0909e441
--- /dev/null
+++ b/assets/resources/subghz/playlist/Lowes_playlist.txt
@@ -0,0 +1,7 @@
+# Lowes Playlist
+sub: /ext/subghz/Stores/Lowes/Packaged_Rugs.sub
+sub: /ext/subghz/Stores/Lowes/Outdoor_Power_Equipment_Desk.sub
+sub: /ext/subghz/Stores/Lowes/Flooring_Desk.sub
+sub: /ext/subghz/Stores/Lowes/Electrical.sub
+sub: /ext/subghz/Stores/Lowes/Blind_Cutting.sub
+sub: /ext/subghz/Stores/Lowes/Appliance_Desk.sub
\ No newline at end of file
diff --git a/assets/resources/subghz/playlist/Walgreens_playlist.txt b/assets/resources/subghz/playlist/Walgreens_playlist.txt
new file mode 100644
index 000000000..2671e1584
--- /dev/null
+++ b/assets/resources/subghz/playlist/Walgreens_playlist.txt
@@ -0,0 +1,4 @@
+# Walgreens Playlist
+sub: /ext/subghz/Stores/Walgreens/Walgreens_Cough.sub
+sub: /ext/subghz/Stores/Walgreens/Walgreens_Skincare.sub
+sub: /ext/subghz/Stores/Walgreens/Walgreens_Vitamin.sub
\ No newline at end of file
diff --git a/documentation/readme.md b/documentation/readme.md
index 3a714a593..53f0011a4 100644
--- a/documentation/readme.md
+++ b/documentation/readme.md
@@ -124,6 +124,8 @@ $ ./fbt resources icons dolphin_ext
- [Tamagotchi(By GMMan)](https://github.com/GMMan/flipperzero-tamagotch-p1)
- [Video Poker (By PixlEmly)](https://github.com/PixlEmly/flipperzero-firmware-testing/blob/420/applications/VideoPoker/poker.c)
- [Yatzee (By emfleak)](https://github.com/emfleak/flipperzero-yatzee)
+- [Pong (By [nmrr])](https://github.com/nmrr/flipperzero-pong)
+- [Asteroids (By [antirez])](https://github.com/antirez/flipper-asteroids)
## Misc