From cae96e57c4516d93d3b66f552b26d6d68149b107 Mon Sep 17 00:00:00 2001 From: Elliot Date: Fri, 15 Jan 2021 15:47:27 -0500 Subject: [PATCH] Big Tetris Parties (#30) Big Tetris Parties TL;DR This patch allows more than the previous 2 players to compete, and makes many minor improvements. - Add magic number to all messages to help catch any network issues - Add message_type field to message wire header, and remove the message_type from the body of many messages - Define new method, message_type_to_str, to print a human-readable message type for logging - Create new helper method message_blob - Declare a macro name "PLAYER_NAME_MAX_CHARS", and use it consistently to validate the player names. - Refactor StringArray to take a max length and define the StringArray serialized header bytes using a struct - Bump MAXMSG up to 2048 - Refactor serialize_state to use no more bytes that necessary, rather than using MAXMSG as before - Server now sends reply to user registration to indicate success - Remove unnecessary and barely-utilized Player->io_lock, since all writes are done with a single call, which POSIX guarantees to be thread-safe. - Add additional safety checks to renderer so that the program doesn't throw nasty errors (segfault) when the renderer has not been properly initialized - Fix bug in read_from_client that referenced the buffer rather than the cursor --- src/CMakeLists.txt | 2 +- src/client.c | 174 ++++++++++++++++++++++++++--------------- src/client_conn.c | 132 +++++++++++++++++-------------- src/client_conn.h | 24 ++++-- src/event.c | 35 +++++++++ src/event.h | 28 +++++++ src/generic.c | 50 ++++++++---- src/generic.h | 5 +- src/list.c | 4 +- src/list.h | 2 +- src/message.c | 93 ++++++++++++++++------ src/message.h | 47 ++++++++++- src/party.c | 33 ++++++++ src/party.h | 38 +++++++++ src/player.c | 74 +++++++++++------- src/player.h | 31 +++++--- src/render.c | 39 +++++++-- src/server.c | 105 ++++++++++++++++++++----- src/test_client_conn.c | 3 +- src/widgets.c | 88 +++++++++++++++++++-- src/widgets.h | 17 +++- 21 files changed, 772 insertions(+), 252 deletions(-) create mode 100644 src/event.c create mode 100644 src/event.h create mode 100644 src/party.c create mode 100644 src/party.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 393b305..2d77356 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -25,7 +25,7 @@ list(APPEND ttetris_SOURCES ${CMAKE_CURRENT_LIST_DIR}/widgets.c ) -add_library(ttetrislib OBJECT ${ttetris_SOURCES} log.h log.c) +add_library(ttetrislib OBJECT ${ttetris_SOURCES} log.h log.c party.c party.h event.c event.h) add_executable(solo_main solo_main.c tetris_game.c) add_executable(client client.c $) diff --git a/src/client.c b/src/client.c index db7ce1c..f9796f4 100644 --- a/src/client.c +++ b/src/client.c @@ -6,6 +6,7 @@ #include #include "client_conn.h" +#include "event.h" #include "log.h" #include "offline.h" #include "player.h" @@ -33,10 +34,12 @@ int run_offline() { char *names[1]; names[0] = "You"; Player *player = player_create(0, names[0]); - start_game(player); + player_game_start(player); render_init(1, names); keyboard_input_loop(offline_control_set(player), NULL); + + player_game_stop(player); render_close(); return EXIT_SUCCESS; } @@ -46,7 +49,10 @@ int run_offline() { */ int run_list_online_players(char *host, int port) { NetClient *net_client = net_client_init(); - tetris_connect(net_client, host, port); + if (tetris_connect(net_client, host, port) == EXIT_FAILURE) { + perror("run_list_online_players"); + return EXIT_FAILURE; + } tetris_listen(net_client); @@ -59,23 +65,51 @@ int run_list_online_players(char *host, int port) { return EXIT_SUCCESS; } +void *player_lobby(void *_net_client) { + NetClient *net_client = (NetClient *)_net_client; + // get the list of possible opponents + StringArray *online_usernames = tetris_list(net_client); + while (online_usernames->length == 0) { + sleep(2); + online_usernames = tetris_list(net_client); + }; + + // null terminate the string array + string_array_resize(online_usernames, online_usernames->length + 1); + + WidgetSelection *selection = ttviz_select( + online_usernames->strings, online_usernames->length, "Opponent", 0); + StringArray *opponents = selection_to_string_array(selection); + tetris_opponent(net_client, opponents); + + tetris_tell_server_to_start(net_client); + + // free up resources + string_array_destroy(online_usernames); + selection_destroy(selection); + + return NULL; +} + /** * run an online game of tetris */ -int run_online(char *host, int port, char *username, char *opponent) { +int run_online(char *host, int port) { // initialize the player list player_init(); - // initialize the renderer - // THIS MUST BE DONE BEFORE REGISTERING WITH THE SERVER - char *names[2]; - names[0] = username; - names[1] = opponent; - render_init(2, names); - // connect to the server NetClient *net_client = net_client_init(host, port); - tetris_connect(net_client, host, port); + if (tetris_connect(net_client, host, port) == EXIT_FAILURE) { + // print the error at the top-left of the terminal using curses + mvprintw(0, 0, strerror(errno)); + return EXIT_FAILURE; + } + tetris_listen(net_client); + + // prompt for a username + char username[32]; + ttviz_entry(username, "Enter username: ", PLAYER_NAME_MAX_CHARS); // create our player Player *player = player_create(0, username); @@ -83,10 +117,23 @@ int run_online(char *host, int port, char *username, char *opponent) { net_client->player = player; // register our player - tetris_register(net_client, username); - tetris_opponent(net_client, opponent); + NetRequest *request = tetris_register(net_client, username); + ttetris_net_request_block_for_response(request); - tetris_listen(net_client); + fprintf( + stderr, + "Registered successfully! Fetching online players from server..."); + + // get the list of possible opponents + + pthread_t _thread; + pthread_create(&_thread, NULL, player_lobby, net_client); + + // block until we hear from the server that the game has started + ttetris_event_block_for_completion(player->game_start_event); + + // cancel the lobby thread if it is still running + pthread_cancel(_thread); // create the renderer and start the input loop keyboard_input_loop(tcp_control_set(), net_client); @@ -97,12 +144,54 @@ int run_online(char *host, int port, char *username, char *opponent) { return EXIT_SUCCESS; } +/** + * Show the main menu for gameplay selection + */ +int main_menu(char *host, int port) { + WidgetSelection *selection; + int selected_choice; + char *choices[] = { + "Single", "Multi", "Controls", "Exit", (char *)NULL, + }; + + while (1) { + // TODO We shouldn't need to be calling all this setup + // everytime. The main change is that the init_pair calls are + // different for the gameplay vs. the menu + initscr(); + start_color(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + init_pair(1, COLOR_RED, COLOR_BLACK); + selection = ttviz_select(choices, 4, "Gameplay Mode", 1); + selected_choice = selection_to_index(selection); + + switch (selected_choice) { + case 0: + run_offline(); + break; + case 1: + run_online(host, port); + break; + case 2: + // TODO Implement the keybindings / controls menu (#21) + break; + case 3: + return EXIT_SUCCESS; + default: + perror("Unexpected state"); + return EXIT_FAILURE; + } + + // TODO Cleanup + } +} + int main(int argc, char *argv[]) { - char username[32] = "Anonymous"; char host[128] = "127.0.0.1"; char port[6] = "5555"; - int should_run_offline = 0; int list_players = 0; // set the logger file pointer to /dev/null @@ -113,14 +202,11 @@ int main(int argc, char *argv[]) { // treated differently than unknown flags. The proceding colons indicate // that flags must have a value. int opt; - while ((opt = getopt(argc, argv, ":hsla:p:")) != -1) { + while ((opt = getopt(argc, argv, ":hla:p:")) != -1) { switch (opt) { case 'h': usage(); break; - case 's': - should_run_offline = 1; - break; case 'a': strncpy(host, optarg, 127); printf("address: %s\n", optarg); @@ -151,50 +237,10 @@ int main(int argc, char *argv[]) { if (list_players) return run_list_online_players(host, numeric_port); - initscr(); - start_color(); - cbreak(); - noecho(); - keypad(stdscr, TRUE); - init_pair(1, COLOR_RED, COLOR_BLACK); - - char *choices[] = { - "Single", - "Multi", - "Exit", - (char *)NULL, - }; - int result = ttviz_select(choices, 4, "Gameplay Mode"); - - // exit chosen - if (result == 2) { - printf("Goodbye!\n"); - exit(0); - } - - // single player mode chosen - if (should_run_offline || result == 0) { - return run_offline(); - } - - ttviz_entry(username, "Enter username: "); - - // get the list of opponents - NetClient *net_client = net_client_init(); - tetris_connect(net_client, host, numeric_port); - tetris_listen(net_client); - StringArray *names = tetris_list(net_client); - tetris_disconnect(net_client); - - char *opponent = ""; - if (names->length > 0) { - // null terminate the string array - string_array_resize(names, names->length + 1); + int exit_code = main_menu(host, numeric_port); - int opponent_index = - ttviz_select(names->strings, names->length, "Opponent"); - opponent = string_array_get_item(names, opponent_index); - } + // make sure terminal is functional when we exit + endwin(); - return run_online(host, numeric_port, username, opponent); + return exit_code; } diff --git a/src/client_conn.c b/src/client_conn.c index 3613167..c554bc9 100644 --- a/src/client_conn.c +++ b/src/client_conn.c @@ -10,6 +10,7 @@ #include #include "client_conn.h" +#include "event.h" #include "log.h" #include "message.h" #include "render.h" @@ -35,9 +36,10 @@ static void init_sockaddr(struct sockaddr_in *name, const char *hostname, name->sin_addr = *(struct in_addr *)hostinfo->h_addr; } -void tetris_send_message(NetClient *net_client, char *body) { +void tetris_send_message(NetClient *net_client, char *body, + msg_type_t message_type) { uint16_t len = strlen(body) + 1; - ttetris_net_request(net_client, body, len); + ttetris_net_request(net_client, body, len, message_type); } /** @@ -54,47 +56,42 @@ int get_socket() { static void tetris_translate(void *net_client, int x) { char xdir = x > 0 ? 1 : 0; - char message[128]; - sprintf(message, "%c%c", MSG_TYPE_TRANSLATE, xdir); - tetris_send_message((NetClient *)net_client, message); + char message[2]; + sprintf(message, "%c", xdir); + tetris_send_message((NetClient *)net_client, message, + MSG_TYPE_TRANSLATE); } static void tetris_lower(void *net_client) { - char message[8]; - sprintf(message, "%c", MSG_TYPE_LOWER); - tetris_send_message((NetClient *)net_client, message); + ttetris_net_request((NetClient *)net_client, NULL, 0, MSG_TYPE_LOWER); } static void tetris_rotate(void *net_client, int theta) { char dir = theta > 0 ? 1 : 0; - char message[128]; - sprintf(message, "%c%c", MSG_TYPE_ROTATE, dir); - tetris_send_message((NetClient *)net_client, message); + char message[2]; + sprintf(message, "%c", dir); + tetris_send_message((NetClient *)net_client, message, MSG_TYPE_ROTATE); } static void tetris_drop(void *net_client) { - char message[128]; - sprintf(message, "%c", MSG_TYPE_DROP); - tetris_send_message((NetClient *)net_client, message); + ttetris_net_request((NetClient *)net_client, NULL, 0, MSG_TYPE_DROP); } static void tetris_swap_hold(void *net_client) { - char message[128]; - sprintf(message, "%c", MSG_TYPE_SWAP_HOLD); - tetris_send_message((NetClient *)net_client, message); + ttetris_net_request((NetClient *)net_client, NULL, 0, + MSG_TYPE_SWAP_HOLD); } StringArray *tetris_list(NetClient *net_client) { - char message[1]; - message[0] = MSG_TYPE_LIST; - NetRequest *request = ttetris_net_request(net_client, message, 1); + NetRequest *request = + ttetris_net_request(net_client, NULL, 0, MSG_TYPE_LIST); ttetris_net_request_block_for_response(request); MessageHeader *header = (MessageHeader *)request->cursor; // deserialize names Blob *body = malloc(sizeof(Blob)); - body->bytes = request->cursor + 1; + body->bytes = request->cursor; body->length = header->content_length; return string_array_deserialize(body); } @@ -102,7 +99,7 @@ StringArray *tetris_list(NetClient *net_client) { /** * establish a connection to the server */ -void tetris_connect(NetClient *net_client, char *host, int port) { +int tetris_connect(NetClient *net_client, char *host, int port) { struct sockaddr_in servername; /* Create the socket. */ @@ -112,29 +109,36 @@ void tetris_connect(NetClient *net_client, char *host, int port) { init_sockaddr(&servername, host, port); if (0 > connect(sock_fd, (struct sockaddr *)&servername, sizeof(servername))) { - perror("connect (client)"); - exit(EXIT_FAILURE); + return EXIT_FAILURE; } net_client->fd = sock_fd; + + return EXIT_SUCCESS; } /** * register */ -void tetris_register(NetClient *net_client, char *username) { - char message[128]; - sprintf(message, "%c%s", MSG_TYPE_REGISTER, username); - tetris_send_message(net_client, message); +NetRequest *tetris_register(NetClient *net_client, char *username) { + char message[16]; + strncpy(message, username, 16); + return ttetris_net_request(net_client, message, 16, MSG_TYPE_REGISTER); } /** * select opponent by username */ -void tetris_opponent(NetClient *net_client, char *username) { - char message[128]; - sprintf(message, "%c%s", MSG_TYPE_OPPONENT, username); - tetris_send_message(net_client, message); +void tetris_opponent(NetClient *net_client, StringArray *usernames) { + Blob *message = string_array_serialize(usernames); + ttetris_net_request(net_client, message->bytes, message->length, + MSG_TYPE_OPPONENT); + free(message->bytes); + free(message); +} + +void tetris_tell_server_to_start(NetClient *net_client) { + message_nbytes(net_client->fd, NULL, 0, 0, MSG_TYPE_START_GAME); } void tetris_disconnect(NetClient *net_client) { @@ -142,13 +146,10 @@ void tetris_disconnect(NetClient *net_client) { net_client->is_listen_thread_started = 0; } -int read_game_view_data(char **cursor, struct game_view_data *view) { - char *buffer = *cursor; - // move the pointer past the message identifier - ++buffer; +static int read_game_view_data(char *buffer, struct game_view_data *view) { // get the player associated with the board char *board_name = buffer; - int name_length = strlen(board_name); + unsigned int name_length = strnlen(board_name, PLAYER_NAME_MAX_CHARS); // move the pointer past the name string buffer += name_length + 1; // copy the game view data @@ -158,19 +159,6 @@ int read_game_view_data(char **cursor, struct game_view_data *view) { return EXIT_SUCCESS; } -int read_names(char **cursor) { - char *buffer = *cursor; - int received_names = *(int *)(buffer + 1); - printf("Num strings received: %d\n", received_names); - char *current_name = buffer + 5; - for (int i = 0; i < received_names; i++) { - printf("Name: %s\n", current_name); - current_name += strlen(current_name) + 1; - } - - return EXIT_SUCCESS; -} - void ttetris_net_request_complete(NetRequest *request) { pthread_mutex_lock(&request->ready_mutex); request->ready_flag = 1; @@ -179,6 +167,7 @@ void ttetris_net_request_complete(NetRequest *request) { }; int read_from_server(NetClient *net_client) { + Blob *blob; char buffer[MAXMSG]; // remember that more than one TCP packet may be read by this command @@ -199,15 +188,45 @@ int read_from_server(NetClient *net_client) { char *cursor = buffer; while (cursor < end) { - MessageHeader *header = (MessageHeader *)buffer; + MessageHeader *header = (MessageHeader *)cursor; + + // Check the magic number, used a mechanism to detect errors. + // For now, we won't fail and exit, but we could consider doing + // that in the future. + if (header->magic_number != MSG_MAGIC_NUMBER) { + fprintf(logging_fp, + "read_from_server: incorrect magic number"); + return EXIT_SUCCESS; + } // increment the cursor to the start of the message body cursor += sizeof(MessageHeader); - switch (cursor[0]) { + // + // This switch statement is essentially for actions that should + // be taken for incoming messages. For synchronous "requests", + // no action is necessary in this switch statement. See + // ttetris_net_request. + // + switch (header->message_type) { + case MSG_TYPE_GAME_STARTED: + fprintf(logging_fp, "read_from_server: game started\n"); + blob = malloc(sizeof(Blob)); + blob->length = header->content_length; + blob->bytes = cursor; + StringArray *party_members = + string_array_deserialize(blob); + render_init(party_members->length, + party_members->strings); + free(blob); + // signal that the game has started + ttetris_event_mark_complete( + net_client->player->game_start_event); + break; case MSG_TYPE_BOARD: - read_game_view_data(&cursor, net_client->player->view); + read_game_view_data(cursor, net_client->player->view); break; + case MSG_TYPE_REGISTER_SUCCESS: case MSG_TYPE_LIST_RESPONSE: break; default: @@ -223,8 +242,7 @@ int read_from_server(NetClient *net_client) { for (int i = 0; i < net_client->open_requests->length; i++) { request = (NetRequest *)list_get( - net_client->open_requests, i) - ->target; + net_client->open_requests, i); if (request->id == header->request_id) { request->cursor = malloc(header->content_length); @@ -282,7 +300,7 @@ NetClient *net_client_init() { }; NetRequest *ttetris_net_request(NetClient *client, char *bytes, - u_int16_t nbytes) { + u_int16_t nbytes, msg_type_t message_type) { NetRequest *request = malloc(sizeof(NetRequest)); // TODO Stop using the number of requests sent as the ID. Of course, we // also want to be able to delete requests from this list at some point. @@ -295,7 +313,7 @@ NetRequest *ttetris_net_request(NetClient *client, char *bytes, // a race condition list_append(client->open_requests, request); - message_nbytes(client->fd, bytes, nbytes, request->id); + message_nbytes(client->fd, bytes, nbytes, request->id, message_type); return request; }; diff --git a/src/client_conn.h b/src/client_conn.h index d563e87..3cc23c5 100644 --- a/src/client_conn.h +++ b/src/client_conn.h @@ -5,6 +5,7 @@ #include "controller.h" #include "list.h" +#include "message.h" #include "player.h" #include "tetris_game.h" @@ -44,7 +45,7 @@ struct ttetris_netrequest { * send a message to the server using the given client connection */ NetRequest *ttetris_net_request(NetClient *client, char *bytes, - u_int16_t nbytes); + u_int16_t nbytes, msg_type_t message_type); /** * set the response for a given net request @@ -62,19 +63,32 @@ void ttetris_net_request_block_for_response(NetRequest *request); NetClient *net_client_init(); -void tetris_send_message(NetClient *net_client, char *body); +void tetris_send_message(NetClient *net_client, char *body, + msg_type_t message_type); -void tetris_connect(NetClient *net_client, char *host, int port); +/** + * Connect to the tetris server + * + * Note that calling this is not enough! You should call tetris_listen after + * this to start the listening thread. + * @param net_client + * @param host + * @param port + * @return + */ +int tetris_connect(NetClient *net_client, char *host, int port); void tetris_disconnect(NetClient *net_client); +void tetris_tell_server_to_start(NetClient *net_client); + StringArray *tetris_list(NetClient *net_client); void tetris_listen(NetClient *net_client); -void tetris_register(NetClient *net_client, char *username); +NetRequest *tetris_register(NetClient *net_client, char *username); -void tetris_opponent(NetClient *net_client, char *username); +void tetris_opponent(NetClient *net_client, StringArray *usernames); TetrisControlSet tcp_control_set(void); diff --git a/src/event.c b/src/event.c new file mode 100644 index 0000000..30e786a --- /dev/null +++ b/src/event.c @@ -0,0 +1,35 @@ +#include +#include +#include + +#include "event.h" + +#define TTETRIS_EVENT_INCOMPLETE 0 +#define TTETRIS_EVENT_COMPLETE 1 + +struct ttetris_event { + int event_state; + pthread_mutex_t ready_mutex; + pthread_cond_t ready_cond; +}; + +TetrisEvent *ttetris_event_create() { + struct ttetris_event *event = calloc(sizeof(struct ttetris_event), 1); + event->event_state = TTETRIS_EVENT_INCOMPLETE; + return event; +} + +void ttetris_event_mark_complete(TetrisEvent *event) { + pthread_mutex_lock(&event->ready_mutex); + event->event_state = TTETRIS_EVENT_COMPLETE; + pthread_cond_broadcast(&event->ready_cond); + pthread_mutex_unlock(&event->ready_mutex); +} + +void ttetris_event_block_for_completion(TetrisEvent *event) { + pthread_mutex_lock(&event->ready_mutex); + while (event->event_state == TTETRIS_EVENT_INCOMPLETE) { + pthread_cond_wait(&event->ready_cond, &event->ready_mutex); + } + pthread_mutex_unlock(&event->ready_mutex); +} \ No newline at end of file diff --git a/src/event.h b/src/event.h new file mode 100644 index 0000000..9d593d4 --- /dev/null +++ b/src/event.h @@ -0,0 +1,28 @@ +/** + * simple library for describing events and waiting for events + */ + +#ifndef TTETRIS_EVENT_H +#define TTETRIS_EVENT_H + +typedef struct ttetris_event TetrisEvent; + +/** + * Allocates an event + * @return event + */ +TetrisEvent *ttetris_event_create(); + +/** + * Mark an event as completed. All threads blocking on the event will resume. + * @param event + */ +void ttetris_event_mark_complete(TetrisEvent *event); + +/** + * Wait for an event to complete + * @param event + */ +void ttetris_event_block_for_completion(TetrisEvent *event); + +#endif // TTETRIS_EVENT_H diff --git a/src/generic.c b/src/generic.c index 39c07cd..9763106 100644 --- a/src/generic.c +++ b/src/generic.c @@ -21,8 +21,17 @@ void shift_blob(Blob *blob, int shift) { blob->length = blob->length + shift; } -StringArray *string_array_create(int length) { +/** + * used when serializing / deserializing a string array + */ +struct ttetris_string_array_wire_header { + int max_string_length; + int length; +}; + +StringArray *string_array_create(int length, int max_string_length) { StringArray *arr = malloc(sizeof(StringArray)); + arr->max_string_length = max_string_length; arr->length = length; arr->strings = calloc(sizeof(char *), length); return arr; @@ -50,10 +59,10 @@ void string_array_resize(StringArray *arr, int new_length) { free(old_strings); }; -void string_array_set_item(StringArray *arr, int index, char *value) { +void string_array_set_item(StringArray *arr, int index, const char *value) { int len = strlen(value); arr->strings[index] = malloc(len + 1); - memcpy(arr->strings[index], value, len); + strncpy(arr->strings[index], value, len); } char *string_array_get_item(StringArray *arr, int index) { @@ -62,22 +71,26 @@ char *string_array_get_item(StringArray *arr, int index) { Blob *string_array_serialize(StringArray *arr) { int message_size = 128; - int message_index = 0; + unsigned int message_index = 0; Blob *blob = create_blob(message_size); - // first four bytes will be the number of strings in the array - *((int *)blob->bytes) = arr->length; - message_index += 4; + // write the wire header + struct ttetris_string_array_wire_header *header = + (struct ttetris_string_array_wire_header *)blob->bytes; + header->max_string_length = arr->max_string_length; + header->length = arr->length; + message_index += sizeof(struct ttetris_string_array_wire_header); // copy the strings into the BLOB for (int i = 0; i < arr->length; i++) { - int len = strlen(arr->strings[i]) + 1; + unsigned int len = + strnlen(arr->strings[i], arr->max_string_length) + 1; // if the blob is not big enough, double it while (message_index + len >= message_size) { message_size *= 2; resize_blob(blob, message_size); } - memcpy(blob->bytes + message_index, arr->strings[i], len); + strncpy(blob->bytes + message_index, arr->strings[i], len); message_index += len; } @@ -85,13 +98,18 @@ Blob *string_array_serialize(StringArray *arr) { } StringArray *string_array_deserialize(Blob *blob) { - int length = *((int *)blob->bytes); - StringArray *arr = string_array_create(length); - - int message_index = 4; - - for (int i = 0; i < length; i++) { - int string_length = strlen(blob->bytes + message_index); + // read the header first + struct ttetris_string_array_wire_header *header = + (struct ttetris_string_array_wire_header *)blob->bytes; + StringArray *arr = + string_array_create(header->length, header->max_string_length); + + // start reading after the header + int message_index = sizeof(struct ttetris_string_array_wire_header); + + for (int i = 0; i < header->length; i++) { + int string_length = strnlen(blob->bytes + message_index, + header->max_string_length); string_array_set_item(arr, i, blob->bytes + message_index); message_index += string_length + 1; } diff --git a/src/generic.h b/src/generic.h index 6534e2e..46d2b07 100644 --- a/src/generic.h +++ b/src/generic.h @@ -23,11 +23,12 @@ void shift_blob(Blob *blob, int shift); * - Resizing this type is also more efficient than using a builtin array */ typedef struct st_string_array { + int max_string_length; int length; char **strings; } StringArray; -StringArray *string_array_create(int length); +StringArray *string_array_create(int length, int max_string_length); void string_array_destroy(StringArray *arr); /** @@ -38,7 +39,7 @@ void string_array_destroy(StringArray *arr); */ void string_array_resize(StringArray *arr, int new_length); -void string_array_set_item(StringArray *arr, int index, char *value); +void string_array_set_item(StringArray *arr, int index, const char *value); char *string_array_get_item(StringArray *arr, int index); Blob *string_array_serialize(StringArray *arr); diff --git a/src/list.c b/src/list.c index 5cb1d09..7ac5d48 100644 --- a/src/list.c +++ b/src/list.c @@ -18,13 +18,13 @@ static struct st_node *list_end_node(struct st_list *list) { return target; } -struct st_node *list_get(struct st_list *list, int index) { +void *list_get(struct st_list *list, int index) { if (index >= list->length) return 0; struct st_node *node = list->head; for (int i = 0; i < index; i++) node = node->next; - return node; + return node->target; } struct st_node *list_search(struct st_list *list, int (*match)(void *)) { diff --git a/src/list.h b/src/list.h index 8499fff..6e499c7 100644 --- a/src/list.h +++ b/src/list.h @@ -15,7 +15,7 @@ struct st_list { List *list_create(); -struct st_node *list_get(struct st_list *list, int index); +void *list_get(struct st_list *list, int index); struct st_node *list_search(struct st_list *list, int (*match)(void *)); diff --git a/src/message.c b/src/message.c index 268539e..58bd173 100644 --- a/src/message.c +++ b/src/message.c @@ -4,28 +4,71 @@ #include #include +#include "log.h" #include "message.h" #include "player.h" #include "tetris_game.h" -/** - * Write n bytes to socket and return EXIT_SUCCESS or EXIT_FAILURE - */ -int message_nbytes(int socket_fd, char *bytes, int nbytes, int request_id) { - int payload_bytes = sizeof(MessageHeader) + nbytes; +char *message_type_to_str(msg_type_t msg_type) { + switch (msg_type) { + case MSG_TYPE_UNKNOWN: + return "UNKNOWN"; + case MSG_TYPE_REGISTER: + return "REGISTER"; + case MSG_TYPE_REGISTER_SUCCESS: + return "REGISTER_SUCCESS"; + case MSG_TYPE_OPPONENT: + return "OPPONENT"; + case MSG_TYPE_ROTATE: + return "ROTATE"; + case MSG_TYPE_TRANSLATE: + return "TRANSLATE"; + case MSG_TYPE_LOWER: + return "LOWER"; + case MSG_TYPE_DROP: + return "DROP"; + case MSG_TYPE_SWAP_HOLD: + return "SWAP_HOLD"; + case MSG_TYPE_LIST: + return "LIST"; + case MSG_TYPE_BOARD: + return "BOARD"; + case MSG_TYPE_LIST_RESPONSE: + return "LIST_RESPONSE"; + case MSG_TYPE_START_GAME: + return "START_GAME"; + case MSG_TYPE_GAME_STARTED: + return "GAME_STARTED"; + default: + return "UNKNOWN"; + } +} - char payload[payload_bytes]; +int message_nbytes(int socket_fd, char *bytes, int nbytes, int request_id, + msg_type_t message_type) { + unsigned long payload_bytes = sizeof(MessageHeader) + nbytes; + char *payload = calloc(sizeof(char), payload_bytes); // write the payload header MessageHeader *header = (MessageHeader *)payload; + header->magic_number = MSG_MAGIC_NUMBER; header->content_length = nbytes; header->request_id = request_id; + header->message_type = message_type; // copy the body into the payload memcpy(payload + sizeof(MessageHeader), bytes, nbytes); int bytes_written = write(socket_fd, payload, payload_bytes); + free(payload); + + fprintf(logging_fp, + "message_nbytes: Wrote %d bytes to file pointer %x, " + "content_length=%d request_id=%d message_type=%s \n", + bytes_written, socket_fd, nbytes, request_id, + message_type_to_str(message_type)); + if (bytes_written < 0) { perror("write"); return EXIT_FAILURE; @@ -34,35 +77,37 @@ int message_nbytes(int socket_fd, char *bytes, int nbytes, int request_id) { return EXIT_SUCCESS; } +int message_blob(int socket_fd, Blob *blob, int request_id, + msg_type_t message_type) { + return message_nbytes(socket_fd, blob->bytes, blob->length, request_id, + message_type); +} + /** * Send a list of all online users over the given socket. */ int send_online_users(int filedes, int request_id) { - StringArray *arr = player_names(); + StringArray *arr = player_names(1); Blob *blob = string_array_serialize(arr); - shift_blob(blob, 1); - blob->bytes[0] = MSG_TYPE_LIST_RESPONSE; - printf("send_online_users: message blob length: %d\n", blob->length); - - message_nbytes(filedes, blob->bytes, blob->length, request_id); - + message_blob(filedes, blob, request_id, MSG_TYPE_LIST_RESPONSE); return EXIT_SUCCESS; } Blob *serialize_state(Player *player) { // first, render the board into the player view generate_game_view_data(&player->view, player->contents); - + // figure out how big our blob needs to be + u_int8_t name_length = strnlen(player->name, PLAYER_NAME_MAX_CHARS); + u_int16_t blob_size = sizeof(struct game_view_data) + name_length + 1; // create a blob to contain the message - Blob *blob = create_blob(MAXMSG); - // first two bytes represent the length of the message - // next byte indicates message type - blob->bytes[0] = MSG_TYPE_BOARD; + Blob *blob = create_blob(blob_size); // next null-terminated bytes are used to store the player name - uint8_t name_length = strlen(player->name); - memcpy(blob->bytes + 1, player->name, name_length + 1); + strncpy(blob->bytes, player->name, name_length + 1); + // strncpy will not null-terminate the string if it is longer than n, + // so this will keep us safe + blob->bytes[name_length] = 0; // the game_view_data is sent directly after the null-byte - memcpy(blob->bytes + 2 + name_length, player->view, + memcpy(blob->bytes + 1 + name_length, player->view, sizeof(struct game_view_data)); return blob; } @@ -95,11 +140,7 @@ int send_player(int socket_fd, struct st_player *player) { Blob *blob = serialize_state(player); - pthread_mutex_lock(&player->io_lock); - int status = message_nbytes(socket_fd, blob->bytes, blob->length, 0); - pthread_mutex_unlock(&player->io_lock); - - return status; + return message_blob(socket_fd, blob, 0, MSG_TYPE_BOARD); } // vi:noet:noai:sw=0:sts=0:ts=8 diff --git a/src/message.h b/src/message.h index 7224bf2..37d28a6 100644 --- a/src/message.h +++ b/src/message.h @@ -4,14 +4,23 @@ #include "player.h" #include "tetris_game.h" -typedef char MessageTypeField[1]; +typedef u_int8_t msg_type_t; + +#define MSG_MAGIC_NUMBER 0xfeedU // the message must be able to hold a 4-byte integer for each cell in the // board, and must have additional space for metadata (such as the player's // name) -#define MAXMSG (4 * BOARD_WIDTH * BOARD_HEIGHT + 256) +#define MAXMSG 2048 +// MSG_TYPE_UNKNOWN should be avoided when possible, but is used to indicate +// any special message that does not conform to one of the standard message +// types +#define MSG_TYPE_UNKNOWN 0 #define MSG_TYPE_REGISTER 'U' +// MSG_TYPE_REGISTER_SUCCESS is sent by the server when a user is successfully +// registered +#define MSG_TYPE_REGISTER_SUCCESS 'V' #define MSG_TYPE_OPPONENT 'O' #define MSG_TYPE_ROTATE 'R' #define MSG_TYPE_TRANSLATE 'T' @@ -21,15 +30,47 @@ typedef char MessageTypeField[1]; #define MSG_TYPE_LIST 'P' #define MSG_TYPE_BOARD 'B' #define MSG_TYPE_LIST_RESPONSE 'Y' +// MSG_TYPE_START_GAME is sent from a client to the server to request that the +// game should begin +#define MSG_TYPE_START_GAME 'A' +// MSG_TYPE_GAME_STARTED is sent from the server to clients when the game has +// been started +#define MSG_TYPE_GAME_STARTED 'C' typedef struct ttetris_msg_header MessageHeader; struct ttetris_msg_header { + /* magic number used to detect if our reader is mis-aligned and + * potentially avoid errors */ + u_int32_t magic_number; + /* id to correlate messages, set to 0 if not needed */ u_int16_t request_id; + /* (required) length of the message body (not including the header) */ u_int16_t content_length; + /* (optional) type of message being sent */ + msg_type_t message_type; }; -int message_nbytes(int socket_fd, char *bytes, int n, int request_id); +/** + * Get a textual representation of a message type + * + * Intended mostly for debugging and logging purposes + * @param msg_type + * @return + */ +char *message_type_to_str(msg_type_t msg_type); + +/** + * Write n bytes to socket and return EXIT_SUCCESS or EXIT_FAILURE + */ +int message_nbytes(int socket_fd, char *bytes, int n, int request_id, + msg_type_t message_type); + +/** + * Wrapper for message_nbytes that takes a blob + */ +int message_blob(int socket_fd, Blob *blob, int request_id, + msg_type_t message_type); int send_online_users(int filedes, int request_id); diff --git a/src/party.c b/src/party.c new file mode 100644 index 0000000..d52c0b9 --- /dev/null +++ b/src/party.c @@ -0,0 +1,33 @@ +#include + +#include "list.h" +#include "party.h" +#include "player.h" + +struct ttetris_party { + List *players; +}; + +struct ttetris_party *ttetris_party_create() { + struct ttetris_party *party = calloc(sizeof(struct ttetris_party), 1); + party->players = list_create(); + return party; +}; + +void ttetris_party_player_add(struct ttetris_party *party, Player *player) { + list_append(party->players, player); + player->party = party; +} + +List *ttetris_party_get_players(struct ttetris_party *party) { + return party->players; +} + +void ttetris_party_start(struct ttetris_party *party) { + Player *player; + + for (int i = 0; i < party->players->length; i++) { + player = (Player *)list_get(party->players, i); + player_game_start(player); + } +} diff --git a/src/party.h b/src/party.h new file mode 100644 index 0000000..85bc0ff --- /dev/null +++ b/src/party.h @@ -0,0 +1,38 @@ +/** + * A tetris party is a group of players that are going to play together. + * + * For example, a player struct may be allocated on the server containing + * three players. Each of these three players will be able to see their + * own board, as well as the boards for the other two players. + */ +#ifndef TTETRIS_PARTY_H +#define TTETRIS_PARTY_H + +#include "list.h" +#include "player.h" + +typedef struct ttetris_party TetrisParty; + +TetrisParty *ttetris_party_create(); + +// forward-definition of Player so that we can do a circular import with +// "player.h" +typedef struct st_player Player; + +void ttetris_party_player_add(TetrisParty *party, Player *player); + +/** + * start the tetris game for all players in the party + * + * ie. + * - start a thread for each player that updates the state of their game board + * - send a message over the network to each player indicating the game has + * started + * - etc. + * @param party + */ +void ttetris_party_start(TetrisParty *party); + +List *ttetris_party_get_players(TetrisParty *party); + +#endif // TTETRIS_PARTY_H diff --git a/src/player.c b/src/player.c index bb41e23..d8d3458 100644 --- a/src/player.c +++ b/src/player.c @@ -4,6 +4,7 @@ #include #include +#include "event.h" #include "generic.h" #include "list.h" #include "log.h" @@ -21,43 +22,71 @@ void *player_clock(void *input) { nanosleep((const struct timespec[]){{0, 500000000L}}, NULL); lower_block(1, player->contents); - // send board to player and clear socket if a failure occurs - if (player->render(player->fd, player) == EXIT_FAILURE) - player->fd = -1; - - // send board to opponent if one exists - if (player->opponent) - player->render(player->opponent->fd, player); + // if the player has a party, send the board to all players + if (player->party) { + List *party_members = + ttetris_party_get_players(player->party); + for (int i = 0; i < party_members->length; i++) + if (player->render( + ((Player *)list_get(party_members, i)) + ->fd, + player) == EXIT_FAILURE) + ((Player *)list_get(party_members, i)) + ->fd = -1; + } + // otherwise, just send the board to the player + else { + if (player->render(player->fd, player) == EXIT_FAILURE) + player->fd = -1; + } } while (game_over(player->contents) == 0); fprintf(logging_fp, "player_clock: thread exiting\n"); return 0; } -void start_game(struct st_player *player) { +void player_game_start(struct st_player *player) { pthread_create(&player->game_clk_thread, NULL, player_clock, (void *)player); } +void player_game_stop(struct st_player *player) { + pthread_cancel(player->game_clk_thread); +} + struct st_player *get_player_from_fd(int fd) { struct st_player *player; for (int i = 0; i < player_list->length; i++) { - player = (struct st_player *)(list_get(player_list, i)->target); + player = (struct st_player *)list_get(player_list, i); if (player->fd == fd) return player; } return 0; } -StringArray *player_names() { - StringArray *arr = string_array_create(player_list->length); +StringArray *player_names(int exclude_in_game) { + int player_index, name_array_index; - for (int i = 0; i < player_list->length; i++) { + StringArray *arr = + string_array_create(player_list->length, PLAYER_NAME_MAX_CHARS); + + name_array_index = 0; + for (player_index = 0; player_index < player_list->length; + player_index++) { Player *player = - (struct st_player *)(list_get(player_list, i)->target); - string_array_set_item(arr, i, player->name); + (struct st_player *)list_get(player_list, player_index); + // skip players with no active socket file descriptor + if (player->fd == -1) + continue; + // skip players that are "in-game" + if (exclude_in_game && player->party != NULL) + continue; + string_array_set_item(arr, name_array_index++, player->name); } + // downsize the string array + string_array_resize(arr, name_array_index); + return arr; } @@ -69,7 +98,7 @@ StringArray *player_names() { Player *player_get_by_name(char *name) { struct st_player *player; for (int i = 0; i < player_list->length; i++) { - player = (struct st_player *)(list_get(player_list, i)->target); + player = (struct st_player *)list_get(player_list, i); fprintf(logging_fp, "player_get_by_name: Checking player '%s'\n", player->name); @@ -86,7 +115,8 @@ struct st_player *player_create(int fd, char *name) { player->fd = fd; player->name = malloc(strlen(name) + 1); memcpy(player->name, name, strlen(name) + 1); - player->opponent = NULL; + player->game_start_event = ttetris_event_create(); + player->party = NULL; /* contents will be initialized by new_game */ player->contents = NULL; player->view = malloc(sizeof(struct game_view_data)); @@ -101,15 +131,3 @@ struct st_player *player_create(int fd, char *name) { return player; } - -void player_set_opponent(Player *player, Player *opponent) { - if (opponent == NULL) - return; - - player->opponent = opponent; - opponent->opponent = player; - - fprintf(logging_fp, - "player_set_opponent: %s and %s are now opponents\n", - player->name, opponent->name); -} diff --git a/src/player.h b/src/player.h index 74eec50..91fd433 100644 --- a/src/player.h +++ b/src/player.h @@ -3,13 +3,26 @@ #include +#include "event.h" #include "generic.h" +#include "party.h" #include "tetris_game.h" -typedef struct st_player { +// does not count the zero-byte / null-terminator +#define PLAYER_NAME_MAX_CHARS 15 + +// forward-definition of TetrisParty so that we can do a circular import with +// "party.h" +typedef struct ttetris_party TetrisParty; + +typedef struct st_player Player; + +struct st_player { char *name; - /* opponent */ - struct st_player *opponent; + /* (optional) party */ + TetrisParty *party; + /* (optional) game start event */ + TetrisEvent *game_start_event; /* the current file descriptor */ int fd; /* reference to the player's rendered board */ @@ -20,9 +33,7 @@ typedef struct st_player { pthread_t game_clk_thread; /* render function */ int (*render)(int socket_fd, struct st_player *); - /* io lock */ - pthread_mutex_t io_lock; -} Player; +}; void player_init(); @@ -30,12 +41,12 @@ struct st_player *get_player_from_fd(int fd); struct st_player *player_create(int fd, char *name); -StringArray *player_names(); +StringArray *player_names(int exclude_in_game); -void start_game(struct st_player *player); +void player_game_start(struct st_player *player); -Player *player_get_by_name(char *name); +void player_game_stop(struct st_player *player); -void player_set_opponent(Player *player, Player *opponent); +Player *player_get_by_name(char *name); #endif diff --git a/src/render.c b/src/render.c index 7668896..71ed8a0 100644 --- a/src/render.c +++ b/src/render.c @@ -26,7 +26,7 @@ struct board_display { WINDOW *lines_window; }; -static struct board_display *boards; +static struct board_display *boards = NULL; static int nboards; // flag used to indicate that the window layout needs to be refreshed static int dirty; @@ -36,10 +36,12 @@ static int too_narrow = 0; static int too_short = 0; static struct board_display *board_from_name(char *name) { + if (boards == NULL) + return NULL; for (int i = 0; i < nboards; i++) if (strcmp(boards[i].name, name) == 0) return boards + i; - return 0; + return NULL; } /** @@ -57,6 +59,8 @@ static void set_window(WINDOW **win, int height, int width, int starty, } void _render_refresh_layout(void) { + if (nboards == 0) + return; // // There are a number of patterns to get ncurses to update the root // window size. @@ -244,13 +248,38 @@ void render_init(int n, char *names[]) { signal(SIGTSTP, render_handle_sigtstp); } -void render_close(void) { - // close the window - // destroy_win(tetris_window); +/** + * cleanup everything associated with rendering the running game (ie. not menus, + * or curses itself, etc) + * + * important: cleanup any other threads that might call the renderer before + * using this method since this method will free memory + */ +void render_ingame_cleanup() { + struct board_display *bd; + + for (int i = 0; i < nboards; i++) { + bd = &boards[i]; + delwin(bd->tetris_window); + delwin(bd->next_block_window); + delwin(bd->hold_block_window); + delwin(bd->points_window); + delwin(bd->lines_window); + } + + free(boards); + boards = NULL; + + // clear the terminal + clear(); + refresh(); + // end curses mode endwin(); } +void render_close(void) { render_ingame_cleanup(); } + /** * Render the terminal background for a coordinate pair in cell-space * diff --git a/src/server.c b/src/server.c index a7cfe55..c58b497 100644 --- a/src/server.c +++ b/src/server.c @@ -9,6 +9,7 @@ #include #include +#include "list.h" #include "log.h" #include "message.h" #include "player.h" @@ -47,11 +48,32 @@ int make_socket(char *host, uint16_t port) { return sock; } +static void tell_party_that_the_game_started(TetrisParty *party) { + int i; + Player *player; + List *players = ttetris_party_get_players(party); + + StringArray *party_members = + string_array_create(players->length, PLAYER_NAME_MAX_CHARS); + for (i = 0; i < players->length; i++) + string_array_set_item(party_members, i, + ((Player *)list_get(players, i))->name); + Blob *party_members_blob = string_array_serialize(party_members); + + for (i = 0; i < players->length; i++) { + player = (Player *)list_get(players, i); + if (player->fd) + message_blob(player->fd, party_members_blob, 0, + MSG_TYPE_GAME_STARTED); + } +} + /** * Returns -1 if EOF is received or 0 otherwise. */ int read_from_client(int filedes) { char buffer[MAXMSG]; + Blob *blob; // remember that more than one TCP packet may be read by this command int nbytes = read(filedes, buffer, MAXMSG); @@ -83,25 +105,35 @@ int read_from_client(int filedes) { while (cursor < end) { MessageHeader *header = (MessageHeader *)buffer; - fprintf(stderr, "read_from_client: id=%d n_bytes=%d\n", - header->request_id, header->content_length); + fprintf(stderr, + "read_from_client: magic=0x%x id=%d n_bytes=%d " + "msg_type=%s\n", + header->magic_number, header->request_id, + header->content_length, + message_type_to_str(header->message_type)); // increment the cursor to the start of the message body cursor += sizeof(MessageHeader); - fprintf(stderr, "message body: %s\n", cursor); - switch (cursor[0]) { + switch (header->message_type) { + case MSG_TYPE_START_GAME: + if (player->party == 0) + break; + ttetris_party_start(player->party); + tell_party_that_the_game_started(player->party); + break; case MSG_TYPE_REGISTER: - sscanf(cursor + 1, "%15s", name); + sscanf(cursor, "%15s", name); player = player_create(filedes, name); - start_game(player); player->render = send_player; + message_nbytes(filedes, NULL, 0, header->request_id, + MSG_TYPE_REGISTER_SUCCESS); break; case MSG_TYPE_ROTATE: - rotate_block(cursor[1], player->contents); + rotate_block(cursor[0], player->contents); break; case MSG_TYPE_TRANSLATE: - translate_block(cursor[1], player->contents); + translate_block(cursor[0], player->contents); break; case MSG_TYPE_LOWER: lower_block(0, player->contents); @@ -113,10 +145,33 @@ int read_from_client(int filedes) { swap_hold_block(player->contents); break; case MSG_TYPE_OPPONENT: - fprintf(stderr, "Opponent: %s\n", cursor + 1); - opponent = player_get_by_name(cursor + 1); - if (opponent) - player_set_opponent(player, opponent); + blob = malloc(sizeof(blob)); + blob->bytes = cursor; + blob->length = header->content_length; + StringArray *opponent_names = + string_array_deserialize(blob); + + TetrisParty *party = ttetris_party_create(); + ttetris_party_player_add(party, player); + + for (int i = 0; i < opponent_names->length; i++) { + + opponent = player_get_by_name( + string_array_get_item(opponent_names, i)); + + if (!opponent) { + fprintf(logging_fp, + "read_from_client: Could " + "not find opponent\n"); + break; + } + fprintf(stderr, + "read_from_client: Adding opponent " + "number %d to party: %s\n", + i, opponent->name); + ttetris_party_player_add(party, opponent); + } + break; case MSG_TYPE_LIST: send_online_users(filedes, header->request_id); @@ -124,21 +179,29 @@ int read_from_client(int filedes) { default: fprintf(stderr, "read_from_client:_received unrecognized " - "message with starting byte 0x%x", - cursor[0]); + "message with message type 0x%x", + header->message_type); } // increment the cursor past the message body end cursor += header->content_length; } - // send the game view data to the player - if (player) - send_player(player->fd, player); - - // send the game view data to the player's opponent - if (player && player->opponent) - send_player(player->opponent->fd, player); + if (player) { + // if the player has a party, send the board to all players + if (player->party) { + List *party_members = + ttetris_party_get_players(player->party); + for (int i = 0; i < party_members->length; i++) + send_player( + ((Player *)list_get(party_members, i))->fd, + player); + } + // otherwise, just send the board to the player + else { + send_player(player->fd, player); + } + } return 0; } diff --git a/src/test_client_conn.c b/src/test_client_conn.c index be3f56e..4e0c5f0 100644 --- a/src/test_client_conn.c +++ b/src/test_client_conn.c @@ -1,4 +1,5 @@ #include "client_conn.h" +#include "message.h" #define PORT 5555 #define HOST "localhost" @@ -10,7 +11,7 @@ int main(void) { tetris_connect(net_client, HOST, PORT); /* Send data to the server. */ - tetris_send_message(net_client, MESSAGE); + tetris_send_message(net_client, MESSAGE, MSG_TYPE_UNKNOWN); tetris_disconnect(net_client); return 0; diff --git a/src/widgets.c b/src/widgets.c index fd83cd7..c572337 100644 --- a/src/widgets.c +++ b/src/widgets.c @@ -1,3 +1,6 @@ +#include "widgets.h" +#include "generic.h" +#include "player.h" #include #include #include @@ -19,7 +22,7 @@ static void strip(char *text) { *c = 0; } -void ttviz_entry(char *username, char *label) { +void ttviz_entry(char *username, char *label, int max_length) { int label_len = strlen(label); FIELD *field[2]; @@ -72,7 +75,7 @@ void ttviz_entry(char *username, char *label) { // buffer form_driver(my_form, REQ_VALIDATION); char *result = field_buffer(field[0], 0); - strcpy(username, result); + strncpy(username, result, max_length); // it seems like the form field always has extra spaces, so get rid of // those strip(username); @@ -86,10 +89,25 @@ void ttviz_entry(char *username, char *label) { endwin(); } +struct ttetris_widget_selection { + /* options is a reference to the options provided by the caller */ + char **options; + int num_options; + int num_selected; + int *indices; +}; + +void selection_destroy(WidgetSelection *selection) { + free(selection->indices); + free(selection); +} + /** * Select from a given number of options */ -int ttviz_select(char **options, int num_options, char *desc) { +struct ttetris_widget_selection *ttviz_select(char **options, int num_options, + char *desc, + int is_single_selection) { int win_width = 20; ITEM **my_items; int c, i; @@ -112,8 +130,18 @@ int ttviz_select(char **options, int num_options, char *desc) { WINDOW *child_win = derwin(my_menu_win, 6, win_width - 2, 3, 1); set_menu_sub(my_menu, child_win); - /* Set menu mark to the string " * " */ - set_menu_mark(my_menu, " * "); + // Set the menu mark. This is shown next to BOTH selected items and the + // current item, which is kind of confusing. Make sure this is at least + // two characters, which will slightly distinguish selected items from + // the current item. + // TODO Find a better multi-select menu. + set_menu_mark(my_menu, "---"); + + if (!is_single_selection) { + mvprintw(1, 1, "Use to select or unselect an item"); + mvprintw(2, 1, "Use to confirm selection"); + menu_opts_off(my_menu, O_ONEVALUE); + } /* Print a border around the main window and print a title */ box(my_menu_win, 0, 0); @@ -136,12 +164,41 @@ int ttviz_select(char **options, int num_options, char *desc) { case KEY_UP: menu_driver(my_menu, REQ_UP_ITEM); break; + case ' ': + // space is not bound for single selection menus + if (!is_single_selection) + menu_driver(my_menu, REQ_TOGGLE_ITEM); + break; } wrefresh(my_menu_win); } - ITEM *cur = current_item(my_menu); - int result = item_index(cur); + WidgetSelection *w_selection = malloc(sizeof(WidgetSelection)); + w_selection->options = options; + w_selection->num_options = num_options; + + ITEM **items = menu_items(my_menu); + + if (is_single_selection) { + ITEM *cur = current_item(my_menu); + w_selection->num_selected = 1; + w_selection->indices = malloc(sizeof(int)); + w_selection->indices[0] = item_index(cur); + } else { + // count the number of items + w_selection->num_selected = 0; + for (i = 0; i < item_count(my_menu); ++i) + if (item_value(items[i]) == TRUE) + w_selection->num_selected += 1; + + // store the selected indices + w_selection->indices = + calloc(sizeof(int), w_selection->num_selected); + int j = 0; + for (i = 0; i < item_count(my_menu); ++i) + if (item_value(items[i]) == TRUE) + w_selection->indices[j++] = i; + } /* Unpost and free all the memory taken up */ unpost_menu(my_menu); @@ -161,7 +218,22 @@ int ttviz_select(char **options, int num_options, char *desc) { free(my_items); - return result; + return w_selection; +} + +int selection_to_index(WidgetSelection *selection) { + return selection->indices[0]; +} + +StringArray *selection_to_string_array(WidgetSelection *selection) { + StringArray *arr = + string_array_create(selection->num_selected, PLAYER_NAME_MAX_CHARS); + + for (int i = 0; i < selection->num_selected; ++i) + string_array_set_item(arr, selection->indices[i], + selection->options[i]); + + return arr; } void print_in_middle(WINDOW *win, int starty, int startx, int width, diff --git a/src/widgets.h b/src/widgets.h index b95fa81..1b51da4 100644 --- a/src/widgets.h +++ b/src/widgets.h @@ -1,3 +1,16 @@ +#ifndef TTETRIS_WIDGETS_H +#define TTETRIS_WIDGETS_H -void ttviz_entry(char *username, char *label); -int ttviz_select(char **options, int num_options, char *desc); +#include "generic.h" + +typedef struct ttetris_widget_selection WidgetSelection; + +int selection_to_index(WidgetSelection *selection); +StringArray *selection_to_string_array(WidgetSelection *selection); +void selection_destroy(WidgetSelection *selection); + +void ttviz_entry(char *username, char *label, int max_length); +WidgetSelection *ttviz_select(char **options, int num_options, char *desc, + int is_single_selection); + +#endif // TTETRIS_WIDGETS_H \ No newline at end of file