Add Snake game (#829)
* Add snake game * Applications: Added a classic game https://en.wikipedia.org/wiki/Snake_(video_game_genre) * Snake Game: Making it impossible to lose button presses * Use more native press button event * Snake Game: use low level InputTypePress event instead of InputTypeShort high level unpredictable event Co-authored-by: LionZXY <nikita@kulikof.ru> Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
		
							parent
							
								
									e54e4a6d77
								
							
						
					
					
						commit
						68274b6c27
					
				| @ -42,6 +42,7 @@ extern int32_t vibro_test_app(void* p); | ||||
| 
 | ||||
| // Plugins
 | ||||
| extern int32_t music_player_app(void* p); | ||||
| extern int32_t snake_game_app(void* p); | ||||
| 
 | ||||
| // On system start hooks declaration
 | ||||
| extern void bt_cli_init(); | ||||
| @ -203,6 +204,10 @@ const FlipperApplication FLIPPER_PLUGINS[] = { | ||||
| #ifdef APP_MUSIC_PLAYER | ||||
|     {.app = music_player_app, .name = "Music Player", .stack_size = 1024, .icon = &A_Plugins_14}, | ||||
| #endif | ||||
| 
 | ||||
| #ifdef APP_SNAKE_GAME | ||||
|     {.app = snake_game_app, .name = "Snake Game", .stack_size = 1024, .icon = &A_Plugins_14}, | ||||
| #endif | ||||
| }; | ||||
| 
 | ||||
| const size_t FLIPPER_PLUGINS_COUNT = sizeof(FLIPPER_PLUGINS) / sizeof(FlipperApplication); | ||||
|  | ||||
| @ -35,6 +35,7 @@ APP_ABOUT	= 1 | ||||
| 
 | ||||
| # Plugins
 | ||||
| APP_MUSIC_PLAYER = 1 | ||||
| APP_SNAKE_GAME = 1 | ||||
| 
 | ||||
| # Debug
 | ||||
| APP_ACCESSOR = 1 | ||||
| @ -185,6 +186,11 @@ CFLAGS		+= -DAPP_MUSIC_PLAYER | ||||
| SRV_GUI		= 1 | ||||
| endif | ||||
| 
 | ||||
| APP_SNAKE_GAME ?= 0 | ||||
| ifeq ($(APP_SNAKE_GAME), 1) | ||||
| CFLAGS		+= -DAPP_SNAKE_GAME | ||||
| SRV_GUI		= 1 | ||||
| endif | ||||
| 
 | ||||
| APP_IBUTTON ?= 0 | ||||
| ifeq ($(APP_IBUTTON), 1) | ||||
|  | ||||
							
								
								
									
										419
									
								
								applications/snake-game/snake-game.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										419
									
								
								applications/snake-game/snake-game.c
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,419 @@ | ||||
| #include <furi.h> | ||||
| #include <gui/gui.h> | ||||
| #include <input/input.h> | ||||
| #include <stdlib.h> | ||||
| 
 | ||||
| typedef struct { | ||||
|     //    +-----x
 | ||||
|     //    |
 | ||||
|     //    |
 | ||||
|     //    y
 | ||||
|     uint8_t x; | ||||
|     uint8_t y; | ||||
| } Point; | ||||
| 
 | ||||
| typedef enum { | ||||
|     GameStateLife, | ||||
| 
 | ||||
|     // https://melmagazine.com/en-us/story/snake-nokia-6110-oral-history-taneli-armanto
 | ||||
|     // Armanto: While testing the early versions of the game, I noticed it was hard
 | ||||
|     // to control the snake upon getting close to and edge but not crashing — especially
 | ||||
|     // in the highest speed levels. I wanted the highest level to be as fast as I could
 | ||||
|     // possibly make the device "run," but on the other hand, I wanted to be friendly
 | ||||
|     // and help the player manage that level. Otherwise it might not be fun to play. So
 | ||||
|     // I implemented a little delay. A few milliseconds of extra time right before
 | ||||
|     // the player crashes, during which she can still change the directions. And if
 | ||||
|     // she does, the game continues.
 | ||||
|     GameStateLastChance, | ||||
| 
 | ||||
|     GameStateGameOver, | ||||
| } GameState; | ||||
| 
 | ||||
| typedef enum { | ||||
|     DirectionUp, | ||||
|     DirectionRight, | ||||
|     DirectionDown, | ||||
|     DirectionLeft, | ||||
| } Direction; | ||||
| 
 | ||||
| #define MAX_SNAKE_LEN 253 | ||||
| 
 | ||||
| typedef struct { | ||||
|     Point points[MAX_SNAKE_LEN]; | ||||
|     uint16_t len; | ||||
|     Direction currentMovement; | ||||
|     Direction nextMovement; // if backward of currentMovement, ignore
 | ||||
|     Point fruit; | ||||
|     GameState state; | ||||
| } SnakeState; | ||||
| 
 | ||||
| typedef enum { | ||||
|     EventTypeTick, | ||||
|     EventTypeKey, | ||||
| } EventType; | ||||
| 
 | ||||
| typedef struct { | ||||
|     EventType type; | ||||
|     InputEvent input; | ||||
| } SnakeEvent; | ||||
| 
 | ||||
| static void snake_game_render_callback(Canvas* const canvas, void* ctx) { | ||||
|     const SnakeState* snake_state = acquire_mutex((ValueMutex*)ctx, 25); | ||||
|     if(snake_state == NULL) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Before the function is called, the state is set with the canvas_reset(canvas)
 | ||||
| 
 | ||||
|     // Frame
 | ||||
|     canvas_draw_frame(canvas, 0, 0, 128, 64); | ||||
| 
 | ||||
|     // Fruit
 | ||||
|     Point f = snake_state->fruit; | ||||
|     f.x = f.x * 4 + 1; | ||||
|     f.y = f.y * 4 + 1; | ||||
|     canvas_draw_rframe(canvas, f.x, f.y, 6, 6, 2); | ||||
| 
 | ||||
|     // Snake
 | ||||
|     for(uint16_t i = 0; i < snake_state->len; i++) { | ||||
|         Point p = snake_state->points[i]; | ||||
|         p.x = p.x * 4 + 2; | ||||
|         p.y = p.y * 4 + 2; | ||||
|         canvas_draw_box(canvas, p.x, p.y, 4, 4); | ||||
|     } | ||||
| 
 | ||||
|     // Game Over banner
 | ||||
|     if(snake_state->state == GameStateGameOver) { | ||||
|         // Screen is 128x64 px
 | ||||
|         canvas_set_color(canvas, ColorWhite); | ||||
|         canvas_draw_box(canvas, 34, 20, 62, 24); | ||||
| 
 | ||||
|         canvas_set_color(canvas, ColorBlack); | ||||
|         canvas_draw_frame(canvas, 34, 20, 62, 24); | ||||
| 
 | ||||
|         canvas_set_font(canvas, FontPrimary); | ||||
|         canvas_draw_str(canvas, 37, 31, "Game Over"); | ||||
| 
 | ||||
|         canvas_set_font(canvas, FontSecondary); | ||||
|         char buffer[12]; | ||||
|         snprintf(buffer, sizeof(buffer), "Score: %u", snake_state->len - 7); | ||||
|         canvas_draw_str_aligned(canvas, 64, 41, AlignCenter, AlignBottom, buffer); | ||||
|     } | ||||
| 
 | ||||
|     release_mutex((ValueMutex*)ctx, snake_state); | ||||
| } | ||||
| 
 | ||||
| static void snake_game_input_callback(InputEvent* input_event, osMessageQueueId_t event_queue) { | ||||
|     furi_assert(event_queue); | ||||
| 
 | ||||
|     SnakeEvent event = {.type = EventTypeKey, .input = *input_event}; | ||||
|     osMessageQueuePut(event_queue, &event, 0, osWaitForever); | ||||
| } | ||||
| 
 | ||||
| static void snake_game_update_timer_callback(osMessageQueueId_t event_queue) { | ||||
|     furi_assert(event_queue); | ||||
| 
 | ||||
|     SnakeEvent event = {.type = EventTypeTick}; | ||||
|     osMessageQueuePut(event_queue, &event, 0, 0); | ||||
| } | ||||
| 
 | ||||
| static void snake_game_init_game(SnakeState* const snake_state) { | ||||
|     Point p[] = {{8, 6}, {7, 6}, {6, 6}, {5, 6}, {4, 6}, {3, 6}, {2, 6}}; | ||||
|     memcpy(snake_state->points, p, sizeof(p)); | ||||
| 
 | ||||
|     snake_state->len = 7; | ||||
| 
 | ||||
|     snake_state->currentMovement = DirectionRight; | ||||
| 
 | ||||
|     snake_state->nextMovement = DirectionRight; | ||||
| 
 | ||||
|     Point f = {18, 6}; | ||||
|     snake_state->fruit = f; | ||||
| 
 | ||||
|     snake_state->state = GameStateLife; | ||||
| } | ||||
| 
 | ||||
| static Point snake_game_get_new_fruit(SnakeState const* const snake_state) { | ||||
|     // 1 bit for each point on the playing field where the snake can turn
 | ||||
|     // and where the fruit can appear
 | ||||
|     uint16_t buffer[8]; | ||||
|     memset(buffer, 0, sizeof(buffer)); | ||||
|     uint8_t empty = 8 * 16; | ||||
| 
 | ||||
|     for(uint16_t i = 0; i < snake_state->len; i++) { | ||||
|         Point p = snake_state->points[i]; | ||||
| 
 | ||||
|         if(p.x % 2 != 0 || p.y % 2 != 0) { | ||||
|             continue; | ||||
|         } | ||||
|         p.x /= 2; | ||||
|         p.y /= 2; | ||||
| 
 | ||||
|         buffer[p.y] |= 1 << p.x; | ||||
|         empty--; | ||||
|     } | ||||
|     // Bit set if snake use that playing field
 | ||||
| 
 | ||||
|     uint16_t newFruit = rand() % empty; | ||||
| 
 | ||||
|     // Skip random number of _empty_ fields
 | ||||
|     for(uint8_t y = 0; y < 8; y++) { | ||||
|         for(uint16_t x = 0, mask = 1; x < 16; x += 1, mask <<= 1) { | ||||
|             if((buffer[y] & mask) == 0) { | ||||
|                 if(newFruit == 0) { | ||||
|                     Point p = { | ||||
|                         .x = x * 2, | ||||
|                         .y = y * 2, | ||||
|                     }; | ||||
|                     return p; | ||||
|                 } | ||||
|                 newFruit--; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     // We will never be here
 | ||||
|     Point p = {0, 0}; | ||||
|     return p; | ||||
| } | ||||
| 
 | ||||
| static bool snake_game_collision_with_frame(Point const next_step) { | ||||
|     // if x == 0 && currentMovement == left then x - 1 == 255 ,
 | ||||
|     // so check only x > right border
 | ||||
|     return next_step.x > 30 || next_step.y > 14; | ||||
| } | ||||
| 
 | ||||
| static bool | ||||
|     snake_game_collision_with_tail(SnakeState const* const snake_state, Point const next_step) { | ||||
|     for(uint16_t i = 0; i < snake_state->len; i++) { | ||||
|         Point p = snake_state->points[i]; | ||||
|         if(p.x == next_step.x && p.y == next_step.y) { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| static Direction snake_game_get_turn_snake(SnakeState const* const snake_state) { | ||||
|     switch(snake_state->currentMovement) { | ||||
|     case DirectionUp: | ||||
|         switch(snake_state->nextMovement) { | ||||
|         case DirectionRight: | ||||
|             return DirectionRight; | ||||
|         case DirectionLeft: | ||||
|             return DirectionLeft; | ||||
|         default: | ||||
|             return snake_state->currentMovement; | ||||
|         } | ||||
|     case DirectionRight: | ||||
|         switch(snake_state->nextMovement) { | ||||
|         case DirectionUp: | ||||
|             return DirectionUp; | ||||
|         case DirectionDown: | ||||
|             return DirectionDown; | ||||
|         default: | ||||
|             return snake_state->currentMovement; | ||||
|         } | ||||
|     case DirectionDown: | ||||
|         switch(snake_state->nextMovement) { | ||||
|         case DirectionRight: | ||||
|             return DirectionRight; | ||||
|         case DirectionLeft: | ||||
|             return DirectionLeft; | ||||
|         default: | ||||
|             return snake_state->currentMovement; | ||||
|         } | ||||
|     default: // case DirectionLeft:
 | ||||
|         switch(snake_state->nextMovement) { | ||||
|         case DirectionUp: | ||||
|             return DirectionUp; | ||||
|         case DirectionDown: | ||||
|             return DirectionDown; | ||||
|         default: | ||||
|             return snake_state->currentMovement; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| static Point snake_game_get_next_step(SnakeState const* const snake_state) { | ||||
|     Point next_step = snake_state->points[0]; | ||||
|     switch(snake_state->currentMovement) { | ||||
|     // +-----x
 | ||||
|     // |
 | ||||
|     // |
 | ||||
|     // y
 | ||||
|     case DirectionUp: | ||||
|         next_step.y--; | ||||
|         break; | ||||
|     case DirectionRight: | ||||
|         next_step.x++; | ||||
|         break; | ||||
|     case DirectionDown: | ||||
|         next_step.y++; | ||||
|         break; | ||||
|     case DirectionLeft: | ||||
|         next_step.x--; | ||||
|         break; | ||||
|     } | ||||
|     return next_step; | ||||
| } | ||||
| 
 | ||||
| static void snake_game_move_snake(SnakeState* const snake_state, Point const next_step) { | ||||
|     memmove(snake_state->points + 1, snake_state->points, snake_state->len * sizeof(Point)); | ||||
|     snake_state->points[0] = next_step; | ||||
| } | ||||
| 
 | ||||
| static void snake_game_process_game_step(SnakeState* const snake_state) { | ||||
|     if(snake_state->state == GameStateGameOver) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     bool can_turn = (snake_state->points[0].x % 2 == 0) && (snake_state->points[0].y % 2 == 0); | ||||
|     if(can_turn) { | ||||
|         snake_state->currentMovement = snake_game_get_turn_snake(snake_state); | ||||
|     } | ||||
| 
 | ||||
|     Point next_step = snake_game_get_next_step(snake_state); | ||||
| 
 | ||||
|     bool crush = snake_game_collision_with_frame(next_step); | ||||
|     if(crush) { | ||||
|         if(snake_state->state == GameStateLife) { | ||||
|             snake_state->state = GameStateLastChance; | ||||
|             return; | ||||
|         } else if(snake_state->state == GameStateLastChance) { | ||||
|             snake_state->state = GameStateGameOver; | ||||
|             return; | ||||
|         } | ||||
|     } else { | ||||
|         if(snake_state->state == GameStateLastChance) { | ||||
|             snake_state->state = GameStateLife; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     crush = snake_game_collision_with_tail(snake_state, next_step); | ||||
|     if(crush) { | ||||
|         snake_state->state = GameStateGameOver; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     bool eatFruit = (next_step.x == snake_state->fruit.x) && (next_step.y == snake_state->fruit.y); | ||||
|     if(eatFruit) { | ||||
|         snake_state->len++; | ||||
|         if(snake_state->len >= MAX_SNAKE_LEN) { | ||||
|             snake_state->state = GameStateGameOver; | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     snake_game_move_snake(snake_state, next_step); | ||||
| 
 | ||||
|     if(eatFruit) { | ||||
|         snake_state->fruit = snake_game_get_new_fruit(snake_state); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| int32_t snake_game_app(void* p) { | ||||
|     srand(DWT->CYCCNT); | ||||
| 
 | ||||
|     osMessageQueueId_t event_queue = osMessageQueueNew(8, sizeof(SnakeEvent), NULL); | ||||
| 
 | ||||
|     SnakeState* snake_state = furi_alloc(sizeof(SnakeState)); | ||||
|     snake_game_init_game(snake_state); | ||||
| 
 | ||||
|     ValueMutex state_mutex; | ||||
|     if(!init_mutex(&state_mutex, snake_state, sizeof(SnakeState))) { | ||||
|         furi_log_print(FURI_LOG_ERROR, "cannot create mutex\r\n"); | ||||
|         free(snake_state); | ||||
|         return 255; | ||||
|     } | ||||
| 
 | ||||
|     ViewPort* view_port = view_port_alloc(); | ||||
|     view_port_draw_callback_set(view_port, snake_game_render_callback, &state_mutex); | ||||
|     view_port_input_callback_set(view_port, snake_game_input_callback, event_queue); | ||||
| 
 | ||||
|     osTimerId_t timer = | ||||
|         osTimerNew(snake_game_update_timer_callback, osTimerPeriodic, event_queue, NULL); | ||||
|     osTimerStart(timer, osKernelGetTickFreq() / 4); | ||||
| 
 | ||||
|     // Open GUI and register view_port
 | ||||
|     Gui* gui = furi_record_open("gui"); | ||||
|     gui_add_view_port(gui, view_port, GuiLayerFullscreen); | ||||
| 
 | ||||
|     SnakeEvent event; | ||||
|     for(bool processing = true; processing;) { | ||||
|         osStatus_t event_status = osMessageQueueGet(event_queue, &event, NULL, 100); | ||||
| 
 | ||||
|         SnakeState* snake_state = (SnakeState*)acquire_mutex_block(&state_mutex); | ||||
| 
 | ||||
|         if(event_status == osOK) { | ||||
|             // press events
 | ||||
|             if(event.type == EventTypeKey) { | ||||
|                 if(event.input.type == InputTypePress) { | ||||
|                     switch(event.input.key) { | ||||
|                     case InputKeyUp: | ||||
|                         snake_state->nextMovement = DirectionUp; | ||||
|                         break; | ||||
|                     case InputKeyDown: | ||||
|                         snake_state->nextMovement = DirectionDown; | ||||
|                         break; | ||||
|                     case InputKeyRight: | ||||
|                         snake_state->nextMovement = DirectionRight; | ||||
|                         break; | ||||
|                     case InputKeyLeft: | ||||
|                         snake_state->nextMovement = DirectionLeft; | ||||
|                         break; | ||||
|                     case InputKeyOk: | ||||
|                         if(snake_state->state == GameStateGameOver) { | ||||
|                             snake_game_init_game(snake_state); | ||||
|                         } | ||||
|                         break; | ||||
|                     case InputKeyBack: | ||||
|                         processing = false; | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } else if(event.type == EventTypeTick) { | ||||
|                 snake_game_process_game_step(snake_state); | ||||
|             } | ||||
|         } else { | ||||
|             // event timeout
 | ||||
|         } | ||||
| 
 | ||||
|         view_port_update(view_port); | ||||
|         release_mutex(&state_mutex, snake_state); | ||||
|     } | ||||
| 
 | ||||
|     osTimerDelete(timer); | ||||
|     view_port_enabled_set(view_port, false); | ||||
|     gui_remove_view_port(gui, view_port); | ||||
|     furi_record_close("gui"); | ||||
|     view_port_free(view_port); | ||||
|     osMessageQueueDelete(event_queue); | ||||
|     delete_mutex(&state_mutex); | ||||
|     free(snake_state); | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| // Screen is 128x64 px
 | ||||
| // (4 + 4) * 16 - 4 + 2 + 2border == 128
 | ||||
| // (4 + 4) * 8 - 4 + 2 + 2border == 64
 | ||||
| // Game field from point{x:  0, y: 0} to point{x: 30, y: 14}.
 | ||||
| // The snake turns only in even cells - intersections.
 | ||||
| // ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // ╎ ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪   ▪ ╎
 | ||||
| // ╎ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ▪ ╎
 | ||||
| // └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘
 | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Oleg Schwann
						Oleg Schwann