diff --git a/cmake/Modules/FindCurses.cmake b/cmake/Modules/FindCurses.cmake new file mode 100644 index 0000000..e6c28c4 --- /dev/null +++ b/cmake/Modules/FindCurses.cmake @@ -0,0 +1,44 @@ +#[=======================================================================[.rst: +FindCurses +---------- + +Find the curses, pdcurses, or ncurses include file and library. + +https://cmake.org/cmake/help/v3.4/manual/cmake-developer.7.html#find-modules + +#]=======================================================================] + +# guard against creating the curses target multiple times +include_guard(GLOBAL) + +set(_PDCURSES_MODULE_NAME curses) +if (PDCURSES_REPO_DIR) + set(_PDCURSES_REPO_DIR ${PDCURSES_REPO_DIR}) +else() + set(_PDCURSES_REPO_DIR $ENV{HOME}/Documents/PDCurses) + message( + WARNING + "The variable PDCURSES_REPO_DIR was not set via a command line argument." + " Guessing ${_PDCURSES_REPO_DIR}" + ) +endif() +get_filename_component(_PDCURSES_ARCHIVE_PATH ${_PDCURSES_REPO_DIR}/wincon/pdcurses.a ABSOLUTE) + +set(CURSES_FOUND True) +set(CURSES_LIBRARY ${_PDCURSES_MODULE_NAME}) +set(CURSES_LIBRARIES ${_PDCURSES_MODULE_NAME}) +set(CURSES_INCLUDE_DIRS ${_PDCURSES_REPO_DIR}) + +# this is a helper function cmake provides to implement expected behavior of a FindXXXX.cmake file +find_package_handle_standard_args(Curses DEFAULT_MSG + CURSES_LIBRARY CURSES_INCLUDE_DIRS) + +include_directories(CURSES_INCLUDE_DIRS) +# setting policy CMP0111 to NEW will mandate that the target IMPORTED_LOCATION is set +cmake_policy(SET CMP0111 NEW) +# STATIC: indicates that this library is composed of archives of object files for use when linking other targets +add_library(${_PDCURSES_MODULE_NAME} STATIC IMPORTED GLOBAL) +set_target_properties(${_PDCURSES_MODULE_NAME} PROPERTIES IMPORTED_LOCATION ${_PDCURSES_ARCHIVE_PATH}) + +unset(_PDCURSES_MODULE_NAME) +unset(_PDCURSES_REPO_DIR) diff --git a/docs/WINDOWS.md b/docs/WINDOWS.md new file mode 100644 index 0000000..e57cdf7 --- /dev/null +++ b/docs/WINDOWS.md @@ -0,0 +1,138 @@ +# Building with Powershell + +This is my preferred approach, using no IDE. + +## 1. Install Chocolatey + +## 2. Install Build Tools + +Install build tools using chocolatey (if not already installed). + +```powershell +choco install git cmake mingw +``` + +## 3. Choose install location + +Change to the directory that you would like to contain the source code. + +For the rest of this guide, we will assume you are using your "Documents" +directory. + +## 4. Clone and Build PDCurses + +```powershell +git.exe clone https://github.com/wmcbrine/PDCurses +``` + +## 5. Clone and Build Tetris + +Clone https://github.com/wmcbrine/PDCurses + +```powershell +git.exe clone https://github.com/nitepone/terminally-tetris +``` + +Clone submodules + +```powershell +git.exe submodule init +git.exe submodule update +``` + +Set the path to the PDCurses repo using an environmental variable. + +Generate our Makefile using CMake. In the command below, replace +`C:/Users/elliot/Documents/PDCurses` with the location that you cloned +PDCurses. Note that *forward slashes* must be used here to comply with cmake. + +```powershell +& "C:\Program Files\CMake\bin\cmake.exe" -G "MinGW Makefiles" -D "PDCURSES_REPO_DIR=C:/Users/elliot/Documents/PDCurses" . +``` + +*Note: Make sure you type the above command exactly as written. The amperstand +tells powershell to treat the quoted path as a program and execute it.* + +*Note: We have to explicitly name the generator we want, in this case +"MinGW Makefiles"* + +Run make + +```powershell +make +``` + +## 6. [Optional] Run Tests + +```powershell +.\bin\unit_tests.exe +``` + +*Note: Some tests (that rely on a specific Linux `rand_r` seeding) will fail on +Windows. Hopefully, we will fix this in the future.* + +### 7. [Optional] Run the server + +If you want to play online, start a server. + +```powershell +.\bin\server.exe +``` + +### 8. Run the Tetris Client Application + +```powershell +.\bin\client.exe +``` + +# Formatting Code with Clang Format + +First, make sure you have chocolatey installed as described under "Building +with Powershell". + +Install LLVM Tools + +```powershell +choco install LLVM +``` + +Change to the root directory of this repo, wherever you cloned it. + +Run clang-format. + +```powershell +& "C:\Program Files\LLVM\bin\clang-format.exe" -style=file -i src/*.c src/*.h +``` + +*Note: Make sure you type the above command exactly as written. The amperstand +tells powershell to treat the quoted path as a program and execute it.* + +# Appendix + +## PDCurses Shortfalls + +### Missing Menu and Forms + +PDCurses does not come with menu and forms like curses does. We may need to +rewrite some code to get around this. + +## Considered Alternatives + +### Cygwin + +The main advantage of Cygwin is that it offers a greater degree of +POSIX-compliance, which might allow us to use fewer pre-processing directives +to support Windows. + +### LLVM + +LLVM seems like it could be a good way to go, although *PDCurses does not come +with an out-of-the-box* Makefile for clang, and after overriding `CC` in the +`GCC` makefile, I found that a number of libaries were missing. We could look +into this more in the future, but it seemed like more work. + +## C Package Managers + +- Open Question: Is there a better way to link PDCurses, or are we stuck + compiling it ourselves? + - Could we use NuGet? diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a2abf2..939b7ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,13 +3,20 @@ cmake_minimum_required (VERSION 3.3) # projectname is the same as the main-executable project(ttetris) -add_definitions('-g') -add_definitions('-Wall') -add_definitions('-std=gnu99') -add_definitions('-fcommon') +# Previously, we used add_definitions() to set the compile flags, but that is not always supported. +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -Wall -fcommon") + +if (WIN32) + # add our custom cmake modules (for curses) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + set(ADDITIONAL_LIBS wsock32 ws2_32) +endif() find_package(Threads) find_package(Curses) +include_directories(${CURSES_INCLUDE_DIRS}) + list(APPEND ttetris_SOURCES ${CMAKE_CURRENT_LIST_DIR}/tetris_game.c @@ -23,20 +30,32 @@ list(APPEND ttetris_SOURCES ${CMAKE_CURRENT_LIST_DIR}/render.c ${CMAKE_CURRENT_LIST_DIR}/widgets.c ${CMAKE_CURRENT_LIST_DIR}/curses_text_entry.c + ${CMAKE_CURRENT_LIST_DIR}/curses_combobox.c + ${CMAKE_CURRENT_LIST_DIR}/terminal_size.c + ${CMAKE_CURRENT_LIST_DIR}/log.c + ${CMAKE_CURRENT_LIST_DIR}/os_compat.c + ${CMAKE_CURRENT_LIST_DIR}/party.c + ${CMAKE_CURRENT_LIST_DIR}/event.c ) -add_library(ttetrislib OBJECT ${ttetris_SOURCES} log.h log.c party.c party.h event.c event.h curses_combobox.c curses_combobox.h) +add_library(ttetrislib OBJECT ${ttetris_SOURCES}) + +# Solo main uses termios, which is *nix only. For now, just skip building +# solo main on Windows. +if (NOT WIN32) + add_executable(solo_main solo_main.c tetris_game.c) + target_link_libraries(solo_main ${CMAKE_THREAD_LIBS_INIT} ) +endif() -add_executable(solo_main solo_main.c tetris_game.c) add_executable(client client.c $) add_executable(server server.c $) add_executable(test_render test_render.c $) add_executable(test_client_conn test_client_conn.c $) add_executable(test_player test_player.c $) -target_link_libraries(solo_main ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(client ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(server ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(test_render ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(test_client_conn ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) -target_link_libraries(test_player ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +target_link_libraries(ttetrislib ${CURSES_LIBRARIES} ${CURSES_INCLUDE_DIRS}) +target_link_libraries(client ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) +target_link_libraries(server ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) +target_link_libraries(test_render ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) +target_link_libraries(test_client_conn ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) +target_link_libraries(test_player ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) diff --git a/src/client.c b/src/client.c index 1a14f8c..0c5dc57 100644 --- a/src/client.c +++ b/src/client.c @@ -9,6 +9,7 @@ #include "event.h" #include "log.h" #include "offline.h" +#include "os_compat.h" #include "player.h" #include "render.h" #include "tetris_game.h" @@ -19,7 +20,7 @@ */ void usage() { fprintf(stderr, - "Usage: ./client [-h] [-l] [-s] [-a ADDRESS] [-p PORT]\n"); + "Usage: ./client [-h] [-f] [-l] [-s] [-a ADDRESS] [-p PORT]\n"); exit(EXIT_FAILURE); } @@ -92,14 +93,16 @@ void *player_lobby(void *_net_client) { * run an online game of tetris */ int run_online(char *host, int port) { + fprintf(logging_fp, "run_online(%s, %d)\n", host, port); // initialize the player list player_init(); // connect to the server NetClient *net_client = net_client_init(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)); + char errmsg[256]; + last_error_message_to_buffer(errmsg, 256); + fprintf(logging_fp, "run_online: %s\n", errmsg); return EXIT_FAILURE; } tetris_listen(net_client); @@ -108,17 +111,21 @@ int run_online(char *host, int port) { char username[32]; ttviz_entry(username, "Enter username: ", PLAYER_NAME_MAX_CHARS); + fprintf(logging_fp, "Entered username=%s\n", username); + // create our player Player *player = player_create(0, username); player->fd = net_client->fd; net_client->player = player; + fprintf(logging_fp, "Sending registration username=%s\n", username); + // register our player NetRequest *request = tetris_register(net_client, username); ttetris_net_request_block_for_response(request); fprintf( - stderr, + logging_fp, "Registered successfully! Fetching online players from server..."); // get the list of possible opponents @@ -129,6 +136,8 @@ int run_online(char *host, int port) { // block until we hear from the server that the game has started ttetris_event_block_for_completion(player->game_start_event); + fprintf(logging_fp, "run_online: unblocked by game start\n"); + // cancel the lobby thread if it is still running pthread_cancel(_thread); @@ -164,6 +173,9 @@ int main_menu(char *host, int port) { selection = ttviz_select(choices, 4, "Gameplay Mode", 1); selected_choice = selection_to_index(selection); + fprintf(logging_fp, "main_menu: selection=%s\n", + choices[selected_choice]); + switch (selected_choice) { case 0: run_offline(); @@ -191,15 +203,18 @@ int main(int argc, char *argv[]) { int list_players = 0; - // set the logger file pointer to /dev/null - logging_set_fp(fopen("/dev/null", "w")); +#ifdef THIS_IS_WINDOWS + char *log_filename = "nul"; +#else + char *log_filename = "/dev/null"; +#endif // Parse command line flags. The optstring passed to getopt has a // preceding colon to tell getopt that missing flag values should be // treated differently than unknown flags. The proceding colons indicate // that flags must have a value. int opt; - while ((opt = getopt(argc, argv, ":hla:p:")) != -1) { + while ((opt = getopt(argc, argv, ":hlf:a:p:")) != -1) { switch (opt) { case 'h': usage(); @@ -208,6 +223,9 @@ int main(int argc, char *argv[]) { strncpy(host, optarg, 127); printf("address: %s\n", optarg); break; + case 'f': + log_filename = optarg; + break; case 'p': strncpy(port, optarg, 5); printf("port: %s\n", optarg); @@ -224,10 +242,20 @@ int main(int argc, char *argv[]) { } } + // set the logger file pointer to /dev/null + FILE *fp = fopen(log_filename, "w"); + logging_set_fp(fp); + // disable buffering on the logging file pointer so that we don't miss + // any data in the event that the program crashes, etc. + setvbuf(fp, NULL, _IONBF, 0); + + fprintf(stderr, "Using logfile %s\n", log_filename); + fprintf(logging_fp, "Using logfile %s\n", log_filename); + // convert the string port to a number port uintmax_t numeric_port = strtoumax(port, NULL, 10); if (numeric_port == UINTMAX_MAX && errno == ERANGE) { - fprintf(stderr, "Provided port is invalid\n"); + fprintf(logging_fp, "Provided port is invalid\n"); usage(); } diff --git a/src/client_conn.c b/src/client_conn.c index c554bc9..927a79d 100644 --- a/src/client_conn.c +++ b/src/client_conn.c @@ -1,14 +1,21 @@ + #include -#include -#include #include #include #include #include -#include #include #include +#include "os_compat.h" +#ifdef THIS_IS_WINDOWS +#include +#else +#include +#include +#include +#endif + #include "client_conn.h" #include "event.h" #include "log.h" @@ -21,9 +28,11 @@ /** * Borrowed from GNU Socket Tutorial + * + * @return EXIT_SUCCESS or EXIT_FAILURE */ -static void init_sockaddr(struct sockaddr_in *name, const char *hostname, - uint16_t port) { +static int init_sockaddr(struct sockaddr_in *name, const char *hostname, + uint16_t port) { struct hostent *hostinfo; name->sin_family = AF_INET; @@ -31,9 +40,11 @@ static void init_sockaddr(struct sockaddr_in *name, const char *hostname, hostinfo = gethostbyname(hostname); if (hostinfo == NULL) { fprintf(logging_fp, "Unknown host %s.\n", hostname); - exit(EXIT_FAILURE); + return EXIT_FAILURE; } name->sin_addr = *(struct in_addr *)hostinfo->h_addr; + + return EXIT_SUCCESS; } void tetris_send_message(NetClient *net_client, char *body, @@ -46,12 +57,23 @@ void tetris_send_message(NetClient *net_client, char *body, * get socket or exit if an error occurs */ int get_socket() { +#ifdef THIS_IS_WINDOWS + WSADATA wsaData; + int startup_result; + // perform the required initialization for winsock + if ((startup_result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) { + fprintf(logging_fp, "WSAStartup failed with error: %d\n", + startup_result); + return EXIT_FAILURE; + } +#endif + int sock = socket(PF_INET, SOCK_STREAM, 0); if (sock >= 0) return sock; perror("socket (client)"); - exit(EXIT_FAILURE); + return EXIT_FAILURE; } static void tetris_translate(void *net_client, int x) { @@ -103,7 +125,7 @@ int tetris_connect(NetClient *net_client, char *host, int port) { struct sockaddr_in servername; /* Create the socket. */ - int sock_fd = get_socket(); + SOCKET sock_fd = get_socket(); /* Connect to the server. */ init_sockaddr(&servername, host, port); @@ -160,29 +182,33 @@ static int read_game_view_data(char *buffer, struct game_view_data *view) { } void ttetris_net_request_complete(NetRequest *request) { - pthread_mutex_lock(&request->ready_mutex); - request->ready_flag = 1; - pthread_cond_broadcast(&request->ready_cond); - pthread_mutex_unlock(&request->ready_mutex); + fprintf(logging_fp, "ttetris_net_request_complete\n"); + ttetris_event_mark_complete(request->response_event); }; int read_from_server(NetClient *net_client) { Blob *blob; char buffer[MAXMSG]; + fprintf(logging_fp, "read_from_server: called\n"); + // remember that more than one TCP packet may be read by this command - int nbytes = read(net_client->fd, buffer, MAXMSG); + int nbytes = recv(net_client->fd, buffer, MAXMSG, 0); - // exit early if there was an error - if (nbytes < 0) { - perror("read"); + if (nbytes == 0) { + // exit early if we reached the end-of-file + fprintf(logging_fp, "read_from_server: received EOF\n"); + return -1; + } else if (nbytes < 0) { + // exit early if there was an error + char errmsg[256]; + last_error_message_to_buffer(errmsg, 256); + fprintf(logging_fp, "read_from_server: %s\n", errmsg); return EXIT_FAILURE; + } else { + fprintf(logging_fp, "read_from_server read %d bytes\n", nbytes); } - // exit early if we reached the end-of-file - if (nbytes == 0) - return -1; - // initialize pointers for moving through data char *end = buffer + nbytes; char *cursor = buffer; @@ -195,10 +221,16 @@ int read_from_server(NetClient *net_client) { // that in the future. if (header->magic_number != MSG_MAGIC_NUMBER) { fprintf(logging_fp, - "read_from_server: incorrect magic number"); + "read_from_server: incorrect magic number\n"); return EXIT_SUCCESS; } + fprintf(logging_fp, + "read_from_server: message type=%s request_id=%d " + "content_length=%d\n", + message_type_to_str(header->message_type), + header->request_id, header->content_length); + // increment the cursor to the start of the message body cursor += sizeof(MessageHeader); @@ -241,6 +273,8 @@ int read_from_server(NetClient *net_client) { NetRequest *request; for (int i = 0; i < net_client->open_requests->length; i++) { + fprintf(logging_fp, + "checking for responses %d\n", i); request = (NetRequest *)list_get( net_client->open_requests, i); if (request->id == header->request_id) { @@ -290,6 +324,7 @@ void tetris_listen(NetClient *net_client) { } NetClient *net_client_init() { + fprintf(logging_fp, "net_client_init()\n"); NetClient *net_client = malloc(sizeof(NetClient)); net_client->is_listen_thread_started = 0; net_client->fd = -1; @@ -299,15 +334,13 @@ NetClient *net_client_init() { return net_client; }; -NetRequest *ttetris_net_request(NetClient *client, char *bytes, - u_int16_t nbytes, msg_type_t message_type) { +NetRequest *ttetris_net_request(NetClient *client, char *bytes, uint16_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. request->id = client->open_requests->length + 1; - request->ready_flag = 0; - pthread_cond_init(&request->ready_cond, NULL); - pthread_mutex_init(&request->ready_mutex, NULL); + request->response_event = ttetris_event_create(); // IMPORTANT: list_append must be called before message_nbytes to avoid // a race condition @@ -319,11 +352,7 @@ NetRequest *ttetris_net_request(NetClient *client, char *bytes, }; void ttetris_net_request_block_for_response(NetRequest *request) { - pthread_mutex_lock(&request->ready_mutex); - while (!request->ready_flag) { - pthread_cond_wait(&request->ready_cond, &request->ready_mutex); - } - pthread_mutex_unlock(&request->ready_mutex); + ttetris_event_block_for_completion(request->response_event); } // define a control set for use over TCP diff --git a/src/client_conn.h b/src/client_conn.h index 3cc23c5..208b9ca 100644 --- a/src/client_conn.h +++ b/src/client_conn.h @@ -1,6 +1,10 @@ #ifndef _CLIENT_CONN_H #define _CLIENT_CONN_H +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#include +#endif + #include #include "controller.h" @@ -13,7 +17,7 @@ typedef struct ttetris_netclient NetClient; struct ttetris_netclient { /* the current file descriptor */ - int fd; + SOCKET fd; // mark whether the listening thread is running. There is no "undefined" // value to store for pthread_t, so we need this. char is_listen_thread_started; @@ -34,18 +38,15 @@ struct ttetris_netrequest { short id; /* pointer to response content */ char *cursor; - /* flag to indicate that we got a response to our request */ - int ready_flag; - /* mutex for the ready flag */ - pthread_mutex_t ready_mutex; - pthread_cond_t ready_cond; + // event indicating when we hear back from the server + TetrisEvent *response_event; }; /** * send a message to the server using the given client connection */ -NetRequest *ttetris_net_request(NetClient *client, char *bytes, - u_int16_t nbytes, msg_type_t message_type); +NetRequest *ttetris_net_request(NetClient *client, char *bytes, uint16_t nbytes, + msg_type_t message_type); /** * set the response for a given net request diff --git a/src/controller.c b/src/controller.c index 4121e8d..91c6a7d 100644 --- a/src/controller.c +++ b/src/controller.c @@ -1,5 +1,5 @@ #include "controller.h" -#include +#include void keyboard_input_loop(TetrisControlSet controls, void *context) { char ch; diff --git a/src/curses_text_entry.c b/src/curses_text_entry.c index 1ebb267..9b814e4 100644 --- a/src/curses_text_entry.c +++ b/src/curses_text_entry.c @@ -155,6 +155,7 @@ int curses_text_field_feed(struct curses_text_entry *entry, int input) { if (entry->entered_text[entry->cursor_position] != 0) entry->cursor_position++; break; + case 8: // case 8 is required for backspace on windows case KEY_BACKSPACE: curses_text_field_delete(entry); break; diff --git a/src/event.c b/src/event.c index 30e786a..347a2c3 100644 --- a/src/event.c +++ b/src/event.c @@ -1,35 +1,73 @@ -#include #include #include -#include "event.h" +#include "os_compat.h" +#ifdef THIS_IS_WINDOWS +#include +#else +#include #define TTETRIS_EVENT_INCOMPLETE 0 #define TTETRIS_EVENT_COMPLETE 1 +#endif + +#include "event.h" struct ttetris_event { +#ifdef THIS_IS_WINDOWS + HANDLE handle; +#else int event_state; pthread_mutex_t ready_mutex; pthread_cond_t ready_cond; +#endif }; +int event_id = 0; + TetrisEvent *ttetris_event_create() { struct ttetris_event *event = calloc(sizeof(struct ttetris_event), 1); +#ifdef THIS_IS_WINDOWS + // create a unique name for the event + char unique_name[7]; + unique_name[0] = (char)('A' + (event_id & 7)); + unique_name[1] = (char)('A' + (event_id >> 4 & 7)); + unique_name[2] = (char)('A' + (event_id >> 8 & 7)); + unique_name[3] = (char)('A' + (event_id >> 12 & 7)); + unique_name[4] = (char)('A' + (event_id >> 16 & 7)); + unique_name[5] = (char)('A' + (event_id >> 20 & 7)); + unique_name[6] = 0; + event_id++; + event->handle = CreateEvent(NULL, // default security attributes + TRUE, // manual-reset event + FALSE, // initial state is nonsignaled + TEXT(unique_name) // object name + ); +#else event->event_state = TTETRIS_EVENT_INCOMPLETE; +#endif return event; } void ttetris_event_mark_complete(TetrisEvent *event) { +#ifdef THIS_IS_WINDOWS + SetEvent(event->handle); +#else pthread_mutex_lock(&event->ready_mutex); event->event_state = TTETRIS_EVENT_COMPLETE; pthread_cond_broadcast(&event->ready_cond); pthread_mutex_unlock(&event->ready_mutex); +#endif } void ttetris_event_block_for_completion(TetrisEvent *event) { +#ifdef THIS_IS_WINDOWS + WaitForSingleObject(event->handle, INFINITE); +#else 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); +#endif } \ No newline at end of file diff --git a/src/event.h b/src/event.h index 9d593d4..0cffed8 100644 --- a/src/event.h +++ b/src/event.h @@ -1,5 +1,5 @@ /** - * simple library for describing events and waiting for events + * simple cross-platform library for describing events and waiting for events */ #ifndef TTETRIS_EVENT_H diff --git a/src/message.c b/src/message.c index 58bd173..fb73458 100644 --- a/src/message.c +++ b/src/message.c @@ -4,6 +4,13 @@ #include #include +#include "os_compat.h" +#ifdef THIS_IS_WINDOWS +#include +#else +#include +#endif + #include "log.h" #include "message.h" #include "player.h" @@ -44,7 +51,7 @@ char *message_type_to_str(msg_type_t msg_type) { } } -int message_nbytes(int socket_fd, char *bytes, int nbytes, int request_id, +int message_nbytes(SOCKET 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); @@ -59,25 +66,32 @@ int message_nbytes(int socket_fd, char *bytes, int nbytes, int request_id, // copy the body into the payload memcpy(payload + sizeof(MessageHeader), bytes, nbytes); - int bytes_written = write(socket_fd, payload, payload_bytes); + int bytes_written = send(socket_fd, payload, payload_bytes, 0); free(payload); + // Note: Windows uses a 64-bit socket number, but Linux uses a 32-bit + // number. For now, this will just print the lower 32 bits. 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)); + bytes_written, (uint32_t)(socket_fd & 0xFFFF), nbytes, + request_id, message_type_to_str(message_type)); +#ifdef THIS_IS_WINDOWS + if (bytes_written == SOCKET_ERROR) { +#else if (bytes_written < 0) { - perror("write"); +#endif + fprintf(logging_fp, + "message_nbytes: socket error during write"); return EXIT_FAILURE; } return EXIT_SUCCESS; } -int message_blob(int socket_fd, Blob *blob, int request_id, +int message_blob(SOCKET 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); @@ -97,8 +111,8 @@ 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; + uint8_t name_length = strnlen(player->name, PLAYER_NAME_MAX_CHARS); + uint16_t blob_size = sizeof(struct game_view_data) + name_length + 1; // create a blob to contain the message Blob *blob = create_blob(blob_size); // next null-terminated bytes are used to store the player name diff --git a/src/message.h b/src/message.h index 37d28a6..e612c39 100644 --- a/src/message.h +++ b/src/message.h @@ -1,10 +1,13 @@ #ifndef MESSAGE_H #define MESSAGE_H +#include + +#include "os_compat.h" #include "player.h" #include "tetris_game.h" -typedef u_int8_t msg_type_t; +typedef uint8_t msg_type_t; #define MSG_MAGIC_NUMBER 0xfeedU @@ -42,11 +45,11 @@ 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; + uint32_t magic_number; /* id to correlate messages, set to 0 if not needed */ - u_int16_t request_id; + uint16_t request_id; /* (required) length of the message body (not including the header) */ - u_int16_t content_length; + uint16_t content_length; /* (optional) type of message being sent */ msg_type_t message_type; }; @@ -61,15 +64,17 @@ struct ttetris_msg_header { char *message_type_to_str(msg_type_t msg_type); /** - * Write n bytes to socket and return EXIT_SUCCESS or EXIT_FAILURE + * Write n bytes to socket + * @return EXIT_SUCCESS or EXIT_FAILURE */ -int message_nbytes(int socket_fd, char *bytes, int n, int request_id, +int message_nbytes(SOCKET socket_fd, char *bytes, int n, int request_id, msg_type_t message_type); /** * Wrapper for message_nbytes that takes a blob + * @return EXIT_SUCCESS or EXIT_FAILURE */ -int message_blob(int socket_fd, Blob *blob, int request_id, +int message_blob(SOCKET 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/os_compat.c b/src/os_compat.c new file mode 100644 index 0000000..c16c7e7 --- /dev/null +++ b/src/os_compat.c @@ -0,0 +1,26 @@ +#include +#include + +#include "os_compat.h" + +#ifdef THIS_IS_WINDOWS +#include +#else +#include +#endif + +void last_error_message_to_buffer(char *buffer, unsigned int max_length) { +#ifdef THIS_IS_WINDOWS + wchar_t wide_char_buffer[256]; + FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, + GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + wide_char_buffer, (sizeof(wide_char_buffer) / sizeof(wchar_t)), + NULL); + wcstombs(buffer, wide_char_buffer, max_length); +#else + strncpy(buffer, strerror(errno), max_length); +#endif + // ensure null termination + buffer[max_length - 1] = 0; +} \ No newline at end of file diff --git a/src/os_compat.h b/src/os_compat.h new file mode 100644 index 0000000..5e719dc --- /dev/null +++ b/src/os_compat.h @@ -0,0 +1,18 @@ +#ifndef TERMINALLY_TETRIS_OS_COMPAT_H +#define TERMINALLY_TETRIS_OS_COMPAT_H + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#define THIS_IS_WINDOWS +// A bad hack, since rand_r is thread safe, and rand is not. +#ifndef rand_r +#define rand_r(n) rand() +#endif +#else +#define THIS_IS_NOT_WINDOWS +typedef int SOCKET; +typedef struct sockaddr_in SOCKADDR_IN; +#endif + +void last_error_message_to_buffer(char *buffer, unsigned int max_length); + +#endif // TERMINALLY_TETRIS_OS_COMPAT_H diff --git a/src/render.c b/src/render.c index a78de38..67068d7 100644 --- a/src/render.c +++ b/src/render.c @@ -1,14 +1,21 @@ -#include +#include #include -#include #include #include #include -#include #include +#include "os_compat.h" +#ifdef THIS_IS_WINDOWS +#include +#include +#else +#include +#endif + #include "log.h" #include "render.h" +#include "terminal_size.h" #include "tetris_game.h" // width of the windows used to show the points, lines, next block, and hold @@ -59,6 +66,8 @@ static void set_window(WINDOW **win, int height, int width, int starty, } void _render_refresh_layout(void) { + fprintf(logging_fp, "_render_refresh_layout nboards=%d\n", nboards); + if (nboards == 0) return; // @@ -76,14 +85,13 @@ void _render_refresh_layout(void) { // next user keypress. Until we find a good solution to that, option #3 // is working nicely. :) // - struct winsize ws; - ioctl(0, TIOCGWINSZ, &ws); + TerminalSize term_size = get_terminal_size(); // Here, we call the "inner" resize_term function rather than the // *recommended* "outer" resizeterm function. This is because in my // testing, resizeterm was causing the next user keypress to get // dropped, which is super annoying. There is probably another, better // solution, but this works fine. :) - resize_term(ws.ws_row, ws.ws_col); + resize_term(term_size.rows, term_size.columns); clear(); @@ -142,6 +150,7 @@ void _render_refresh_layout(void) { dirty = false; } +#ifdef THIS_IS_NOT_WINDOWS /** * This signal handler is just slightly different than the one that comes with * ncurses. @@ -160,20 +169,21 @@ void (*cached_handler)(int); * @param handler */ static void get_current_handler(int signal, void (**handler)(int)) { - struct sigaction query_action; - if (sigaction(signal, NULL, &query_action) < 0) { - // -1 indicates an error - *handler = NULL; - } else if (query_action.sa_handler == SIG_DFL) { - // default handler - *handler = NULL; - } else if (query_action.sa_handler == SIG_IGN) { - // signal ignored - *handler = NULL; - } else { - // a custom handler is defined and in effect - *handler = query_action.sa_handler; - } + // struct sigaction query_action; + // if (sigaction(signal, NULL, &query_action) < 0) { + // // -1 indicates an error + // *handler = NULL; + // } else if (query_action.sa_handler == SIG_DFL) { + // // default handler + // *handler = NULL; + // } else if (query_action.sa_handler == SIG_IGN) { + // // signal ignored + // *handler = NULL; + // } else { + // // a custom handler is defined and in effect + // *handler = query_action.sa_handler; + // } + *handler = NULL; } /** @@ -189,12 +199,14 @@ static void render_handle_sigtstp(int sig) { if (cached_handler) cached_handler(sig); } +#endif /** * initialize the renderer to display n games for players given by the names in * the array names */ void render_init(int n, char *names[]) { + fprintf(logging_fp, "render_init(n=%d, names=...)\n", n); // If the locale is not initialized, the library assumes that characters // are printable as in ISO-8859-1, to work with certain legacy programs. // This is to be explicit. @@ -240,6 +252,7 @@ void render_init(int n, char *names[]) { _render_refresh_layout(); +#ifdef THIS_IS_NOT_WINDOWS signal(SIGWINCH, render_handle_sig); // cache the ncurses SIGTSTP handler so that we can call it right after @@ -247,6 +260,7 @@ void render_init(int n, char *names[]) { get_current_handler(SIGTSTP, &cached_handler); // set our handler for SIGTSTP signal(SIGTSTP, render_handle_sigtstp); +#endif } /** @@ -381,8 +395,13 @@ void render_game_view_data(char *name, struct game_view_data *view) { if (bd == 0) { fprintf(logging_fp, - "render_game_view_data: no board found for name\n"); + "render_game_view_data: no board found for name %s\n", + name); return; + } else { + fprintf(logging_fp, + "render_game_view_data: rendering board for %s\n", + name); } WINDOW *tetris_window = bd->tetris_window; diff --git a/src/render.h b/src/render.h index 4e0aa81..499fa91 100644 --- a/src/render.h +++ b/src/render.h @@ -1,7 +1,10 @@ #ifndef _RENDER_HEADER #define _RENDER_HEADER -#include +// Before including curses, undefine MOUSE_MOVED if provided by windows.h +// TODO look for a more elegant solution +#undef MOUSE_MOVED +#include #include "tetris_game.h" diff --git a/src/server.c b/src/server.c index e9045e2..90fd2f8 100644 --- a/src/server.c +++ b/src/server.c @@ -1,33 +1,60 @@ -#include -#include #include #include -#include #include #include #include -#include + #include +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#define THIS_IS_WINDOWS +#include +#include + +#else +#include +#include +#include +#include +#endif + #include "list.h" #include "log.h" #include "message.h" +#include "os_compat.h" #include "player.h" /** - * get and bind a socket or exit on failure + * get and bind a socket + * @param host string IP address + * @param port numeric port + * @return EXIT_SUCCESS or EXIT_FAILURE */ int make_socket(char *host, uint16_t port) { int sock; struct sockaddr_in name; +#ifdef THIS_IS_WINDOWS + WSADATA wsaData; + int startup_result; + // perform the required initialization for winsock + if ((startup_result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) { + fprintf(logging_fp, "WSAStartup failed with error: %d\n", + startup_result); + return EXIT_FAILURE; + } +#endif + // get a socket sock = socket(PF_INET, SOCK_STREAM, 0); if (sock < 0) { - perror("socket"); + char errmsg[256]; + last_error_message_to_buffer(errmsg, 256); + fprintf(stderr, errmsg); exit(EXIT_FAILURE); } +#ifdef THIS_IS_NOT_WINDOWS // forcefully attaching socket to the port int opt = 1; if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, @@ -35,16 +62,28 @@ int make_socket(char *host, uint16_t port) { perror("setsockopt"); exit(EXIT_FAILURE); } +#endif - /* Give the socket a name. */ + // Give the socket a name. name.sin_family = AF_INET; name.sin_port = htons(port); + +#ifdef THIS_IS_WINDOWS + WSAStringToAddress((LPSTR)host, AF_INET, NULL, (LPSOCKADDR)&name, + (LPINT)sizeof(name)); +#else inet_pton(AF_INET, host, &name.sin_addr.s_addr); + +#endif + if (bind(sock, (struct sockaddr *)&name, sizeof(name)) < 0) { perror("bind"); - exit(EXIT_FAILURE); + return EXIT_FAILURE; } + fprintf(logging_fp, "make_socket: binding successful to %s:%d\n", host, + port); + return sock; } @@ -71,16 +110,17 @@ static void tell_party_that_the_game_started(TetrisParty *party) { /** * Returns -1 if EOF is received or 0 otherwise. */ -int read_from_client(int filedes) { +int read_from_client(SOCKET 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); + // int nbytes = read(filedes, buffer, MAXMSG); + int nbytes = recv(filedes, buffer, MAXMSG, 0); // exit early if there was an error if (nbytes < 0) { - perror("read"); + perror("read_from_client: read"); exit(EXIT_FAILURE); } @@ -252,12 +292,12 @@ int main(int argc, char *argv[]) { // convert the string port to a number port uintmax_t numeric_port = strtoumax(port, NULL, 10); if (numeric_port == UINTMAX_MAX && errno == ERANGE) { - fprintf(stderr, "Provided port is invalid\n"); + fprintf(logging_fp, "Provided port is invalid\n"); usage(); } - int sock; - fd_set active_fd_set, read_fd_set; + SOCKET sock; + fd_set active_fd_set; int i; struct sockaddr_in clientname; size_t size; @@ -269,54 +309,82 @@ int main(int argc, char *argv[]) { exit(EXIT_FAILURE); } - /* Initialize the set of active sockets. */ - FD_ZERO(&active_fd_set); - FD_SET(sock, &active_fd_set); + fprintf(logging_fp, "main: Started listening\n"); + + // TODO What is the maximum number of sockets we can put in a file + // descriptor set? + int max_sockets = 50; + SOCKET client_socket[max_sockets]; + for (i = 0; i < max_sockets; i++) + client_socket[i] = 0; /* Initialize the player list */ player_init(); while (1) { - /* Block until input arrives on one or more active sockets. */ - read_fd_set = active_fd_set; - if (select(FD_SETSIZE, &read_fd_set, NULL, NULL, NULL) < 0) { + // clear the socket fd set + FD_ZERO(&active_fd_set); + + // add the main listening socket to our active set + FD_SET(sock, &active_fd_set); + + // add (non-NULL) child sockets to the active fd set + for (i = 0; i < max_sockets; i++) + if (client_socket[i] > 0) + FD_SET(client_socket[i], &active_fd_set); + + // Block until input arrives on one or more active sockets. + if (select(FD_SETSIZE, &active_fd_set, NULL, NULL, NULL) < 0) { perror("select"); exit(EXIT_FAILURE); } - /* Service all the sockets with input pending. */ - for (i = 0; i < FD_SETSIZE; ++i) { + // service the listening socket + // + // for new connections: + // - accept the connection + // - create a file descriptor for the connection + // - add the file descriptor to the file descriptor set + if (FD_ISSET(sock, &active_fd_set)) { + size = sizeof(clientname); + SOCKET new = + accept(sock, (struct sockaddr *)&clientname, + (socklen_t *)&size); + if (new < 0) { + perror("accept"); + exit(EXIT_FAILURE); + } + fprintf(logging_fp, + "main: new connection from host %s, " + "port %hu.\n", + inet_ntoa(clientname.sin_addr), + ntohs(clientname.sin_port)); + + FD_SET(new, &active_fd_set); + + // put the socket in our manual list of sockets + for (i = 0; i < max_sockets; i++) + if (client_socket[i] == 0) { + client_socket[i] = new; + break; + } + } + + // service all the sockets with previously accepted connections + // that have input pending + for (i = 0; i < max_sockets; i++) { + SOCKET s = client_socket[i]; + // exit early if the file descriptor i is not in the set - if (!FD_ISSET(i, &read_fd_set)) + if (!FD_ISSET(s, &active_fd_set)) continue; - // for new connections: - // - accept the connection - // - create a file descriptor for the connection - // - add the file descriptor to the file descriptor set - if (i == sock) { - size = sizeof(clientname); - int new = - accept(sock, (struct sockaddr *)&clientname, - (socklen_t *)&size); - if (new < 0) { - perror("accept"); - exit(EXIT_FAILURE); - } - fprintf(stderr, - "main: new connection from host %s, " - "port %hu.\n", - inet_ntoa(clientname.sin_addr), - ntohs(clientname.sin_port)); - - FD_SET(new, &active_fd_set); - } // handle data on sockets already in the file descriptor // set - else if (read_from_client(i) < 0) { - fprintf(stderr, "main: received EOF\n"); + if (read_from_client(s) < 0) { + fprintf(logging_fp, "main: received EOF\n"); close(i); - FD_CLR(i, &active_fd_set); + client_socket[i] = 0; } } } diff --git a/src/terminal_size.c b/src/terminal_size.c new file mode 100644 index 0000000..ba57e6f --- /dev/null +++ b/src/terminal_size.c @@ -0,0 +1,26 @@ +#include "os_compat.h" +#ifdef THIS_IS_WINDOWS +#include +#else +#include +#endif + +#include "terminal_size.h" + +TerminalSize get_terminal_size() { + TerminalSize size; + +#ifdef THIS_IS_WINDOWS + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); + size.columns = csbi.srWindow.Right - csbi.srWindow.Left + 1; + size.rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; +#else + struct winsize ws; + ioctl(0, TIOCGWINSZ, &ws); + size.columns = ws.ws_col; + size.rows = ws.ws_row; +#endif + + return size; +} \ No newline at end of file diff --git a/src/terminal_size.h b/src/terminal_size.h new file mode 100644 index 0000000..4fb7584 --- /dev/null +++ b/src/terminal_size.h @@ -0,0 +1,13 @@ +#ifndef _TERMINAL_SIZE_H +#define _TERMINAL_SIZE_H + +#include + +typedef struct terminal_size { + uint16_t rows; + uint16_t columns; +} TerminalSize; + +TerminalSize get_terminal_size(); + +#endif \ No newline at end of file diff --git a/src/tetris_game.c b/src/tetris_game.c index 635dd88..caf34fd 100644 --- a/src/tetris_game.c +++ b/src/tetris_game.c @@ -9,6 +9,7 @@ #include #include +#include "os_compat.h" #include "tetris_game.h" /* diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e293913..7cdb0eb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,10 +1,21 @@ cmake_minimum_required (VERSION 3.3) +# Previously, we used add_definitions() to set the compile flags, but that is not always supported. +set(CMAKE_C_STANDARD 99) + +if (WIN32) + # add our custom cmake modules (for curses) + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + set(ADDITIONAL_LIBS wsock32 ws2_32) +endif() + find_package(Threads) find_package(Curses) +include_directories(${CURSES_INCLUDE_DIRS}) + add_executable(unit_tests test_tetris_game.c $) -target_link_libraries(unit_tests ttetrislib unity ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT}) +target_link_libraries(unit_tests ttetrislib unity ${CURSES_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} ${ADDITIONAL_LIBS}) add_test(test_basic test_tetris_game)