diff --git a/Makefile b/Makefile index 894fcc3..1cc8248 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ LDFLAGS ?= LDFLAGS += ${USER_LDFLAGS} OBJ = autocomplete.o avatars.o bootstrap.o chat.o chat_commands.o configdir.o curl_util.o execute.o -OBJ += file_transfers.o friendlist.o game_base.o game_centipede.o game_chess.o game_util.o game_snake.o -OBJ += global_commands.o conference_commands.o conference.o help.o input.o line_info.o log.o message_queue.o +OBJ += file_transfers.o friendlist.o game_base.o game_centipede.o game_chess.o game_util.o game_snake.o +OBJ += global_commands.o conference_commands.o conference.o help.o input.o line_info.o log.o message_queue.o OBJ += misc_tools.o name_lookup.o notify.o prompt.o qr_code.o settings.o term_mplex.o toxic.o toxic_strings.o windows.o # Check if debug build is enabled diff --git a/src/chat.c b/src/chat.c index d2d2be1..b9ae10b 100644 --- a/src/chat.c +++ b/src/chat.c @@ -35,6 +35,7 @@ #include "execute.h" #include "file_transfers.h" #include "friendlist.h" +#include "game_base.h" #include "help.h" #include "input.h" #include "line_info.h" @@ -88,6 +89,7 @@ static const char *chat_cmd_list[] = { "/nick", "/note", "/nospam", + "/play", "/quit", "/savefile", "/sendfile", @@ -757,6 +759,75 @@ static void chat_onConferenceInvite(ToxWindow *self, Tox *m, int32_t friendnumbe line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/join\" to join the chat."); } +void chat_onGameInvite(ToxWindow *self, Tox *m, uint32_t friend_number, const uint8_t *data, size_t length) +{ + if (!self || self->num != friend_number) { + return; + } + + if (length < GAME_PACKET_HEADER_SIZE) { + return; + } + + uint8_t version = data[0]; + + if (version != GAME_NETWORKING_VERSION) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, + "Game invite failed. Friend has network protocol version %d, you have version %d.", version, GAME_NETWORKING_VERSION); + return; + } + + GameType type = data[1]; + + if (!game_type_is_multiplayer(type)) { + return; + } + + uint32_t id; + game_util_unpack_u32(data + 2, &id); + + const char *game_string = game_get_name_string(type); + + if (game_string == NULL) { + return; + } + + Friends.list[friend_number].game_invite.type = type; + Friends.list[friend_number].game_invite.id = id; + Friends.list[friend_number].game_invite.pending = true; + + uint32_t data_length = length - GAME_PACKET_HEADER_SIZE; + Friends.list[friend_number].game_invite.data_length = data_length; + + if (data_length > 0) { + free(Friends.list[friend_number].game_invite.data); + + uint8_t *buf = calloc(1, data_length); + + if (buf == NULL) { + return; + } + + memcpy(buf, data + GAME_PACKET_HEADER_SIZE, data_length); + Friends.list[friend_number].game_invite.data = buf; + } + + char name[TOX_MAX_NAME_LENGTH]; + get_nick_truncate(m, name, friend_number); + + if (self->active_box != -1) { + box_notify2(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, self->active_box, + "invites you to play %s", game_string); + } else { + box_notify(self, generic_message, NT_WNDALERT_2 | user_settings->bell_on_invite, &self->active_box, name, + "invites you to play %s", game_string); + } + + + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "%s has invited you to a game of %s.", name, game_string); + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Type \"/play\" to join the game."); +} + /* AV Stuff */ #ifdef AUDIO @@ -1475,6 +1546,7 @@ ToxWindow *new_chat(Tox *m, uint32_t friendnum) ret->onFileControl = &chat_onFileControl; ret->onFileRecv = &chat_onFileRecv; ret->onReadReceipt = &chat_onReadReceipt; + ret->onGameInvite = &chat_onGameInvite; #ifdef AUDIO ret->onInvite = &chat_onInvite; diff --git a/src/chat_commands.c b/src/chat_commands.c index fff1329..fad1515 100644 --- a/src/chat_commands.c +++ b/src/chat_commands.c @@ -172,6 +172,59 @@ void cmd_conference_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char #endif } +void cmd_game_join(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) +{ + UNUSED_VAR(window); + UNUSED_VAR(m); + + bool force_small = false; + + if (argc >= 2) { + force_small = strcasecmp(argv[2], "small") == 0; + } + + if (!Friends.list[self->num].game_invite.pending) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "No pending game invite."); + return; + } + + if (get_num_active_windows() >= MAX_WINDOWS_NUM) { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, RED, " * Warning: Too many windows are open."); + return; + } + + GameType type = Friends.list[self->num].game_invite.type; + uint32_t id = Friends.list[self->num].game_invite.id; + uint8_t *data = Friends.list[self->num].game_invite.data; + size_t length = Friends.list[self->num].game_invite.data_length; + + int ret = game_initialize(self, m, type, id, data, length, force_small); + + switch (ret) { + case 0: { + free(data); + Friends.list[self->num].game_invite.data = NULL; + Friends.list[self->num].game_invite.pending = false; + break; + } + + case -1: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Window is too small. Try enlarging your window."); + return; + } + + case -2: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (network error)"); + return; + } + + default: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (error %d)", ret); + return; + } + } +} + void cmd_savefile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { UNUSED_VAR(window); diff --git a/src/chat_commands.h b/src/chat_commands.h index 1c71686..9d0160e 100644 --- a/src/chat_commands.h +++ b/src/chat_commands.h @@ -29,6 +29,7 @@ void cmd_cancelfile(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_invite(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_conference_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +void cmd_game_join(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_savefile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); void cmd_sendfile(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); diff --git a/src/execute.c b/src/execute.c index 5636415..108747a 100644 --- a/src/execute.c +++ b/src/execute.c @@ -81,6 +81,7 @@ static struct cmd_func chat_commands[] = { { "/cancel", cmd_cancelfile }, { "/invite", cmd_conference_invite }, { "/join", cmd_conference_join }, + { "/play", cmd_game_join }, { "/savefile", cmd_savefile }, { "/sendfile", cmd_sendfile }, #ifdef AUDIO diff --git a/src/friendlist.c b/src/friendlist.c index 8b2e10d..59be9c8 100644 --- a/src/friendlist.c +++ b/src/friendlist.c @@ -116,8 +116,9 @@ static void realloc_blocklist(int n) void kill_friendlist(ToxWindow *self) { for (size_t i = 0; i < Friends.max_idx; ++i) { - if (Friends.list[i].active && Friends.list[i].conference_invite.key != NULL) { + if (Friends.list[i].active) { free(Friends.list[i].conference_invite.key); + free(Friends.list[i].game_invite.data); } } @@ -582,6 +583,34 @@ static void friendlist_add_blocked(uint32_t fnum, uint32_t bnum) } } +static void friendlist_onGameInvite(ToxWindow *self, Tox *m, uint32_t friend_number, const uint8_t *data, size_t length) +{ + UNUSED_VAR(self); + UNUSED_VAR(data); + UNUSED_VAR(length); + + if (friend_number >= Friends.max_idx) { + return; + } + + if (Friends.list[friend_number].chatwin != -1) { + return; + } + + if (get_num_active_windows() < MAX_WINDOWS_NUM) { + Friends.list[friend_number].chatwin = add_window(m, new_chat(m, Friends.list[friend_number].num)); + return; + } + + char nick[TOX_MAX_NAME_LENGTH]; + get_nick_truncate(m, nick, friend_number); + + line_info_add(prompt, false, NULL, NULL, SYS_MSG, 0, RED, + "* Game invite from %s failed: Too many windows are open.", nick); + + sound_notify(prompt, notif_error, NT_WNDALERT_1, NULL); +} + static void friendlist_onFileRecv(ToxWindow *self, Tox *m, uint32_t num, uint32_t filenum, uint64_t file_size, const char *filename, size_t name_length) { @@ -1368,6 +1397,7 @@ ToxWindow *new_friendlist(void) ret->onStatusMessageChange = &friendlist_onStatusMessageChange; ret->onFileRecv = &friendlist_onFileRecv; ret->onConferenceInvite = &friendlist_onConferenceInvite; + ret->onGameInvite = &friendlist_onGameInvite; #ifdef AUDIO ret->onInvite = &friendlist_onAV; diff --git a/src/friendlist.h b/src/friendlist.h index dd57bcb..b89930b 100644 --- a/src/friendlist.h +++ b/src/friendlist.h @@ -26,6 +26,7 @@ #include #include "file_transfers.h" +#include "game_base.h" #include "toxic.h" #include "windows.h" @@ -42,6 +43,14 @@ struct ConferenceInvite { bool pending; }; +struct GameInvite { + uint8_t *data; + size_t data_length; + GameType type; + uint32_t id; + bool pending; +}; + typedef struct { char name[TOXIC_MAX_NAME_LENGTH + 1]; uint16_t namelength; @@ -58,6 +67,7 @@ typedef struct { struct LastOnline last_online; struct ConferenceInvite conference_invite; + struct GameInvite game_invite; struct FileTransfer file_receiver[MAX_FILES]; struct FileTransfer file_sender[MAX_FILES]; diff --git a/src/game_base.c b/src/game_base.c index be23c78..cc0fa54 100644 --- a/src/game_base.c +++ b/src/game_base.c @@ -25,6 +25,7 @@ #include #include +#include "friendlist.h" #include "game_centipede.h" #include "game_base.h" #include "game_chess.h" @@ -32,6 +33,8 @@ #include "line_info.h" #include "misc_tools.h" +extern struct Winthread Winthread; + /* * Determines the base rate at which game objects should update their state. * Inversely correlated with frame rate. @@ -60,7 +63,7 @@ && ((max_x) >= (GAME_MAX_RECT_X_SMALL))) -static ToxWindow *game_new_window(GameType type); +static ToxWindow *game_new_window(GameType type, uint32_t friendnumber); struct GameList { const char *name; @@ -74,13 +77,6 @@ static struct GameList game_list[] = { { NULL, GT_Invalid }, }; - -static void game_get_parent_max_x_y(const ToxWindow *parent, int *max_x, int *max_y) -{ - getmaxyx(parent->window, *max_y, *max_x); - *max_y -= (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT); -} - /* * Returns the GameType associated with `game_string`. */ @@ -97,8 +93,7 @@ GameType game_get_type(const char *game_string) return GT_Invalid; } -/* Returns the string name associated with `type`. */ -static const char *game_get_name_string(GameType type) +const char *game_get_name_string(GameType type) { GameType match_type; @@ -111,9 +106,6 @@ static const char *game_get_name_string(GameType type) return NULL; } -/* - * Prints all available games to window associated with `self`. - */ void game_list_print(ToxWindow *self) { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Available games:"); @@ -125,11 +117,16 @@ void game_list_print(ToxWindow *self) } } +bool game_type_is_multiplayer(GameType type) +{ + return type == GT_Chess; +} + /* Returns the current wall time in milliseconds */ TIME_MS get_time_millis(void) { struct timespec t; - timespec_get(&t, TIME_UTC); + clock_gettime(CLOCK_MONOTONIC, &t); return ((TIME_MS) t.tv_sec) * 1000 + ((TIME_MS) t.tv_nsec) / 1000000; } @@ -164,9 +161,9 @@ static void game_toggle_pause(GameData *game) } } -static int game_initialize_type(GameData *game) +static int game_initialize_type(GameData *game, const uint8_t *data, size_t length) { - int ret = -1; + int ret = -3; switch (game->type) { case GT_Snake: { @@ -180,7 +177,7 @@ static int game_initialize_type(GameData *game) } case GT_Chess: { - ret = chess_initialize(game); + ret = chess_initialize(game, data, length); break; } @@ -192,18 +189,14 @@ static int game_initialize_type(GameData *game) return ret; } -/* - * Initializes game instance. - * - * Return 0 on success. - * Return -1 if screen is too small. - * Return -2 on other failure. - */ -int game_initialize(const ToxWindow *parent, Tox *m, GameType type, bool force_small_window) +int game_initialize(const ToxWindow *parent, Tox *m, GameType type, uint32_t id, const uint8_t *multiplayer_data, + size_t length, bool force_small_window) { int max_x; int max_y; - game_get_parent_max_x_y(parent, &max_x, &max_y); + getmaxyx(parent->window, max_y, max_x); + + max_y -= (CHATBOX_HEIGHT + WINDOW_BAR_HEIGHT); int max_game_window_x = GAME_MAX_SQUARE_X; int max_game_window_y = GAME_MAX_SQUARE_Y; @@ -221,10 +214,10 @@ int game_initialize(const ToxWindow *parent, Tox *m, GameType type, bool force_s } } - ToxWindow *self = game_new_window(type); + ToxWindow *self = game_new_window(type, parent->num); if (self == NULL) { - return -2; + return -4; } GameData *game = self->game; @@ -234,27 +227,47 @@ int game_initialize(const ToxWindow *parent, Tox *m, GameType type, bool force_s if (window_id == -1) { free(game); free(self); - return -2; + return -4; } - game->parent = parent; + game->is_multiplayer = game_type_is_multiplayer(type); + + if (game->is_multiplayer) { + if (parent->type != WINDOW_TYPE_CHAT) { + game_kill(self); + return -3; + } + + if (get_friend_connection_status(parent->num) == TOX_CONNECTION_NONE) { + return -2; + } + + game->is_multiplayer = true; + } + + game->tox = m; game->window_shape = GW_ShapeSquare; game->game_max_x = max_game_window_x; game->game_max_y = max_game_window_y; + game->parent_max_x = max_x; + game->parent_max_y = max_y; game->update_interval = GAME_DEFAULT_UPDATE_INTERVAL; game->type = type; game->window_id = window_id; - game->level = 0; game->window = subwin(self->window, max_y, max_x, 0, 0); + game->id = id; + game->friend_number = parent->num; if (game->window == NULL) { game_kill(self); - return -2; + return -4; } - if (game_initialize_type(game) == -1) { + int init_ret = game_initialize_type(game, multiplayer_data, length); + + if (init_ret < 0) { game_kill(self); - return -1; + return init_ret; } game->status = GS_Running; @@ -283,11 +296,8 @@ int game_set_window_shape(GameData *game, GameWindowShape shape) return 0; } - const ToxWindow *parent = game->parent; - - int max_x; - int max_y; - game_get_parent_max_x_y(parent, &max_x, &max_y); + const int max_x = game->parent_max_x; + const int max_y = game->parent_max_y; if (WINDOW_SIZE_LARGE_RECT_VALID(max_x, max_y)) { game->game_max_x = GAME_MAX_RECT_X; @@ -439,14 +449,14 @@ static int game_restart(GameData *game) game_clear_all_messages(game); - if (game_initialize_type(game) == -1) { + if (game_initialize_type(game, NULL, 0) == -1) { return -1; } return 0; } -static void game_draw_help_bar(WINDOW *win) +static void game_draw_help_bar(const GameData *game, WINDOW *win) { int max_x; int max_y; @@ -456,10 +466,12 @@ static void game_draw_help_bar(WINDOW *win) wmove(win, max_y - 1, 1); - wprintw(win, "Pause: "); - wattron(win, A_BOLD); - wprintw(win, "F2 "); - wattroff(win, A_BOLD); + if (!game->is_multiplayer) { + wprintw(win, "Pause: "); + wattron(win, A_BOLD); + wprintw(win, "F2 "); + wattroff(win, A_BOLD); + } wprintw(win, "Quit: "); wattron(win, A_BOLD); @@ -628,13 +640,13 @@ void game_onDraw(ToxWindow *self, Tox *m) { UNUSED_VAR(m); // Note: This function is not thread safe if we ever need to use `m` - game_draw_help_bar(self->window); + GameData *game = self->game; + + game_draw_help_bar(game, self->window); draw_window_bar(self); curs_set(0); - GameData *game = self->game; - int max_x; int max_y; getmaxyx(game->window, max_y, max_x); @@ -678,8 +690,8 @@ void game_onDraw(ToxWindow *self, Tox *m) bool game_onKey(ToxWindow *self, Tox *m, wint_t key, bool is_printable) { - UNUSED_VAR(m); // Note: this function is not thread safe if we ever need to use `m` UNUSED_VAR(is_printable); + UNUSED_VAR(m); GameData *game = self->game; @@ -688,12 +700,12 @@ bool game_onKey(ToxWindow *self, Tox *m, wint_t key, bool is_printable) return true; } - if (key == KEY_F(2)) { + if (!game->is_multiplayer && key == KEY_F(2)) { game_toggle_pause(self->game); return true; } - if (game->status == GS_Finished && key == KEY_F(5)) { + if (!game->is_multiplayer && game->status == GS_Finished && key == KEY_F(5)) { if (game_restart(self->game) == -1) { fprintf(stderr, "Warning: game_restart() failed\n"); } @@ -702,7 +714,15 @@ bool game_onKey(ToxWindow *self, Tox *m, wint_t key, bool is_printable) } if (game->cb_game_key_press) { + if (game->is_multiplayer) { + pthread_mutex_lock(&Winthread.lock); // we use the tox instance when we send packets + } + game->cb_game_key_press(game, key, game->cb_game_key_press_data); + + if (game->is_multiplayer) { + pthread_mutex_unlock(&Winthread.lock); + } } return true; @@ -723,7 +743,55 @@ void game_onInit(ToxWindow *self, Tox *m) self->window_bar = subwin(self->window, WINDOW_BAR_HEIGHT, max_x, max_y - 2, 0); } -static ToxWindow *game_new_window(GameType type) +/* + * Byte 0: Game type + * Byte 1-4: Game ID + * Byte 5-* Game data + */ +void game_onPacket(ToxWindow *self, Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length) +{ + UNUSED_VAR(m); + + GameData *game = self->game; + + if (friendnumber != self->num) { + return; + } + + if (data == NULL) { + return; + } + + if (length < GAME_PACKET_HEADER_SIZE || length > GAME_MAX_PACKET_SIZE) { + return; + } + + if (data[0] != GAME_NETWORKING_VERSION) { + return; + } + + GameType type = (GameType)data[1]; + + if (game->type != type) { + return; + } + + uint32_t id; + game_util_unpack_u32(data + 2, &id); + + if (game->id != id) { + return; + } + + data += GAME_PACKET_HEADER_SIZE; + length -= GAME_PACKET_HEADER_SIZE; + + if (game->cb_game_on_packet) { + game->cb_game_on_packet(game, data, length, game->cb_game_on_packet_data); + } +} + +static ToxWindow *game_new_window(GameType type, uint32_t friendnumber) { const char *window_name = game_get_name_string(type); @@ -737,11 +805,13 @@ static ToxWindow *game_new_window(GameType type) return NULL; } + ret->num = friendnumber; ret->type = WINDOW_TYPE_GAME; ret->onInit = &game_onInit; ret->onDraw = &game_onDraw; ret->onKey = &game_onKey; + ret->onGameData = &game_onPacket; ret->game = calloc(1, sizeof(GameData)); @@ -941,3 +1011,58 @@ void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data) game->cb_game_pause_data = cb_data; } +void game_set_cb_on_packet(GameData *game, cb_game_on_packet *func, void *cb_data) +{ + game->cb_game_on_packet = func; + game->cb_game_on_packet_data = cb_data; +} + +/* + * Wraps `packet` in a header comprised of the custom packet type, game type and game id. + */ +static int game_wrap_packet(const GameData *game, uint8_t *packet, size_t size, GamePacketType packet_type) +{ + if (size < GAME_PACKET_HEADER_SIZE + 1) { + return -1; + } + + if (packet_type != GP_Invite && packet_type != GP_Data) { + return -1; + } + + packet[0] = packet_type == GP_Data ? CUSTOM_PACKET_GAME_DATA : CUSTOM_PACKET_GAME_INVITE; + packet[1] = GAME_NETWORKING_VERSION; + packet[2] = game->type; + + game_util_pack_u32(packet + 3, game->id); + + return 0; +} + +int game_send_packet(const GameData *game, const uint8_t *data, size_t length, GamePacketType packet_type) +{ + if (length > GAME_MAX_DATA_SIZE) { + return -1; + } + + uint8_t packet[GAME_MAX_PACKET_SIZE]; + + if (game_wrap_packet(game, packet, sizeof(packet), packet_type) == -1) { + return -1; + } + + size_t packet_length = 1 + GAME_PACKET_HEADER_SIZE; // 1 extra byte for custom packet type + + memcpy(packet + 1 + GAME_PACKET_HEADER_SIZE, data, length); + packet_length += length; + + TOX_ERR_FRIEND_CUSTOM_PACKET err; + + if (!tox_friend_send_lossless_packet(game->tox, game->friend_number, packet, packet_length, &err)) { + fprintf(stderr, "failed to send game packet: error %d\n", err); + return -1; + } + + return -0; +} + diff --git a/src/game_base.h b/src/game_base.h index 678932a..e6028db 100644 --- a/src/game_base.h +++ b/src/game_base.h @@ -52,14 +52,35 @@ /* Maximum length of a game message set with game_set_message() */ #define GAME_MAX_MESSAGE_SIZE 64 +/* Default number of seconds a game message is displayed for */ #define GAME_MESSAGE_DEFAULT_TIMEOUT 3 + +/***** NETWORKING DEFINES *****/ + +/* Header starts after custom packet type byte. Comprised of: NetworkVersion (1b) + GameType (1b) + id (4b) */ +#define GAME_PACKET_HEADER_SIZE (1 + 1 + sizeof(uint32_t)) + +/* Max size of a game packet including the header */ +#define GAME_MAX_PACKET_SIZE 1024 + +/* Max size of a game packet payload */ +#define GAME_MAX_DATA_SIZE (GAME_MAX_PACKET_SIZE - GAME_PACKET_HEADER_SIZE - 1) + +/* Current version of networking protocol */ +#define GAME_NETWORKING_VERSION 0x01 + typedef void cb_game_update_state(GameData *game, void *cb_data); typedef void cb_game_render_window(GameData *game, WINDOW *window, void *cb_data); typedef void cb_game_kill(GameData *game, void *cb_data); typedef void cb_game_pause(GameData *game, bool is_paused, void *cb_data); typedef void cb_game_key_press(GameData *game, int key, void *cb_data); +typedef void cb_game_on_packet(GameData *game, const uint8_t *data, size_t length, void *cb_data); +typedef enum GamePacketType { + GP_Invite = 0u, + GP_Data, +} GamePacketType; typedef enum GameWindowShape { GW_ShapeSquare = 0u, @@ -105,6 +126,7 @@ struct GameData { size_t level; GameStatus status; GameType type; + bool is_multiplayer; bool show_lives; bool show_score; @@ -116,11 +138,20 @@ struct GameData { int game_max_x; // max dimensions of game window int game_max_y; + + int parent_max_x; // max dimensions of parent window + int parent_max_y; + int window_id; WINDOW *window; - const ToxWindow *parent; + + Tox *tox; // must be locked with Winthread mutex + GameWindowShape window_shape; + uint32_t id; // indentifies multiplayer instance + uint32_t friend_number; // friendnumber associated with parent window + cb_game_update_state *cb_game_update_state; void *cb_game_update_state_data; @@ -135,6 +166,9 @@ struct GameData { cb_game_key_press *cb_game_key_press; void *cb_game_key_press_data; + + cb_game_on_packet *cb_game_on_packet; + void *cb_game_on_packet_data; }; @@ -163,14 +197,32 @@ void game_set_cb_on_pause(GameData *game, cb_game_pause *func, void *cb_data); */ void game_set_cb_on_keypress(GameData *game, cb_game_key_press *func, void *cb_data); +/* + * Sets the callback for the game packet event. + */ +void game_set_cb_on_packet(GameData *game, cb_game_on_packet *func, void *cb_data); + /* * Initializes game instance. * + * `type` must be a valid GameType. + * + * `id` should be a unique integer to indentify the game instance. If we're being invited to a game + * this identifier should be sent via the invite packet. + * + * `force_small_window` will make the game window small. + * + * if `multiplayer_data` is non-null this indicates that we accepted a game invite from a contact. + * The data contains any information we need to initialize the game state. + * * Return 0 on success. * Return -1 if screen is too small. - * Return -2 on other failure. + * Return -2 on network related error. + * Return -3 if multiplayer game is being initialized outside of a contact's window. + * Return -4 on other failure. */ -int game_initialize(const ToxWindow *self, Tox *m, GameType type, bool force_small_window); +int game_initialize(const ToxWindow *self, Tox *m, GameType type, uint32_t id, const uint8_t *multiplayer_data, + size_t length, bool force_small_window); /* * Sets game window to `shape` and attempts to adjust size for best fit. @@ -188,11 +240,21 @@ int game_set_window_shape(GameData *game, GameWindowShape shape); */ GameType game_get_type(const char *game_string); +/* + * Returns the name represented as a string associated with `type`. + */ +const char *game_get_name_string(GameType type); + /* * Prints all available games to window associated with `self`. */ void game_list_print(ToxWindow *self); +/* + * Return true if game `type` has a multiplayer mode. + */ +bool game_type_is_multiplayer(GameType type); + /* * Returns true if coordinates designated by `x` and `y` are within the game window boundaries. */ @@ -308,4 +370,15 @@ TIME_MS get_time_millis(void); */ void game_kill(ToxWindow *self); +/* + * Sends a packet containing payload `data` of size `length` to the friendnumber associated with the game's + * parent window. + * + * `length` must not exceed GAME_MAX_DATA_SIZE bytes. + * + * `packet_type` should be GP_Invite for an invite packet or GP_Data for all other game data. + */ +int game_send_packet(const GameData *game, const uint8_t *data, size_t length, GamePacketType packet_type); + #endif // GAME_BASE + diff --git a/src/game_chess.c b/src/game_chess.c index ab820a3..a3d757c 100644 --- a/src/game_chess.c +++ b/src/game_chess.c @@ -39,9 +39,16 @@ #define CHESS_SQUARES (CHESS_BOARD_ROWS * CHESS_BOARD_COLUMNS) #define CHESS_MAX_MESSAGE_SIZE 64 +typedef enum ChessPacketType { + CHESS_PACKET_INIT_SEND_INVITE = 0x01, + CHESS_PACKET_INIT_ACCEPT_INVITE = 0x02, + CHESS_PACKET_MOVE_PIECE = 0xFE, + CHESS_PACKET_RESIGN = 0xFF, +} ChessPacketType; + typedef struct ChessCoords { char L; - int N; + uint8_t N; } ChessCoords; typedef enum ChessColour { @@ -50,9 +57,11 @@ typedef enum ChessColour { } ChessColour; typedef enum ChessStatus { - Playing = 0u, + Initializing = 0U, + Playing, Checkmate, Stalemate, + Resigned, } ChessStatus; typedef enum PieceType { @@ -100,6 +109,7 @@ typedef struct Player { Piece captured[CHESS_SQUARES]; size_t number_captured; + int score; // total points of pieces captured } Player; typedef struct ChessState { @@ -118,10 +128,14 @@ typedef struct ChessState { ChessStatus status; } ChessState; + +static int chess_packet_send_move(const GameData *game, const Tile *from, const Tile *to); +static int chess_packet_send_resign(const GameData *game); + + static const char Board_Letters[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}; #define CHESS_NUM_BOARD_LETTERS (sizeof(Board_Letters) / sizeof(char)) - static int chess_get_letter_index(char letter) { for (int i = 0; i < CHESS_NUM_BOARD_LETTERS; ++i) { @@ -133,6 +147,14 @@ static int chess_get_letter_index(char letter) return -1; } +/* + * Copies `piece_b` to `piece_a`. + */ +static void chess_copy_piece(Piece *piece_a, Piece *piece_b) +{ + memcpy(piece_a, piece_b, sizeof(Piece)); +} + static void chess_set_piece(Piece *piece, PieceType type, ChessColour colour) { piece->type = type; @@ -169,6 +191,50 @@ static void chess_set_piece(Piece *piece, PieceType type, ChessColour colour) } } +static size_t chess_get_piece_value(PieceType type) +{ + switch (type) { + case Pawn: + return 1; + + case Bishop: + return 3; + + case Knight: + return 3; + + case Rook: + return 5; + + case Queen: + return 9; + + default: + return 0; + } +} + +/* + * Puts the absolute difference between `from` and `to` chess coordinates in `l_diff` and `n_diff`. + * + * Return 0 on success. + * Return -1 on failure. + */ +static int chess_get_chess_coords_diff(const Tile *from, const Tile *to, int *l_diff, int *n_diff) +{ + int from_letter_idx = chess_get_letter_index(from->chess_coords.L); + int to_letter_idx = chess_get_letter_index(to->chess_coords.L); + + if (from_letter_idx == -1 || to_letter_idx == -1) { + return -1; + } + + *l_diff = abs(from_letter_idx - to_letter_idx); + *n_diff = abs((int)from->chess_coords.N - (int)to->chess_coords.N); + + return 0; +} + static void chess_set_status_message(ChessState *state, const char *message, size_t length) { if (length > CHESS_MAX_MESSAGE_SIZE) { @@ -180,6 +246,48 @@ static void chess_set_status_message(ChessState *state, const char *message, siz state->message_length = length; } +static void chess_print_move_notation(ChessState *state, const Tile *from, const Tile *to, bool check) +{ + if (from->piece.type == King) { // special case if player castled + int l_diff; + int n_diff; + + if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { + chess_set_status_message(state, "Error", strlen("Error")); + return; + } + + if (n_diff == 0 && l_diff > 1 && (to->chess_coords.L == 'c' || to->chess_coords.L == 'g')) { + const char *message = to->chess_coords.L == 'c' ? "Last move: 0-0-0" : "Last move: 0-0"; + chess_set_status_message(state, message, strlen(message)); + return; + } + } + + char message[CHESS_MAX_MESSAGE_SIZE + 1]; + + bool captured = (to->piece.type != NoPiece) || (from->piece.type == Pawn && from->chess_coords.L != to->chess_coords.L); + + char tmp[2]; + snprintf(tmp, sizeof(tmp), "%c", from->piece.display_char); + + const char *from_char = from->piece.type != Pawn ? tmp : ""; + + char pawn_capture[2] = {0}; + + if (strcmp(from_char, "") == 0 && captured) { + snprintf(pawn_capture, sizeof(pawn_capture), "%c", from->chess_coords.L); + } + + const char *capture = captured ? "x" : ""; + const char *check_char = check ? "+" : ""; + + snprintf(message, sizeof(message), "Last move: %s%s%s%c%u%s", + pawn_capture, from_char, capture, to->chess_coords.L, to->chess_coords.N, check_char); + + chess_set_status_message(state, message, strlen(message)); +} + /* * Return true if `pair_a` is the same as `pair_b`. */ @@ -212,6 +320,31 @@ static Player *chess_get_player_to_move(ChessState *state) } } +/* + * Return true if it's `player`'s turn to move. + */ +static bool chess_player_to_move(const ChessState *state, const Player *player) +{ + return (player->colour == White && !state->black_to_move) || (player->colour == Black && state->black_to_move); +} + +/* + * Removes `piece` from the board and puts it in `player`'s captured list. Also updates their score. + */ +static void chess_capture_piece(Player *player, Piece *piece) +{ + size_t idx = player->number_captured; + + if (idx < CHESS_SQUARES) { + chess_copy_piece(&player->captured[idx], piece); + ++player->number_captured; + } + + player->score += chess_get_piece_value(piece->type); + + piece->type = NoPiece; +} + /* * Puts coordinates associated with tile at x y coordinates in `chess_coords`. * @@ -281,27 +414,6 @@ static Tile *chess_get_tile_at_chess_coords(Board *board, const ChessCoords *che return NULL; } -/* - * Puts the absolute difference between `from` and `to` chess coordinates in `l_diff` and `n_diff`. - * - * Return 0 on success. - * Return -1 on failure. - */ -static int chess_get_chess_coords_diff(const Tile *from, const Tile *to, int *l_diff, int *n_diff) -{ - int from_letter_idx = chess_get_letter_index(from->chess_coords.L); - int to_letter_idx = chess_get_letter_index(to->chess_coords.L); - - if (from_letter_idx == -1 || to_letter_idx == -1) { - return -1; - } - - *l_diff = abs(from_letter_idx - to_letter_idx); - *n_diff = abs(from->chess_coords.N - to->chess_coords.N); - - return 0; -} - /* * Return true if `piece` can occupy `tile`. */ @@ -387,6 +499,7 @@ static bool chess_path_diagonal_clear(Board *board, const Tile *from, const Tile size_t start = 1 + MIN(from->chess_coords.N, to->chess_coords.N); size_t end = start + n_diff - 1; + // we're caluclating from south-east to north-west, or from south-west to north-east bool left_diag = (from->chess_coords.N > to->chess_coords.N && from->chess_coords.L < to->chess_coords.L) || (from->chess_coords.N < to->chess_coords.N && from->chess_coords.L > to->chess_coords.L); @@ -431,10 +544,10 @@ static bool chess_path_diagonal_clear(Board *board, const Tile *from, const Tile * * Should be called after every successful move. */ -static void chess_player_clear_en_passant(Player *player) +static void chess_player_check_en_passant(Player *player) { if (player->en_passant_move_number == -1) { - chess_set_piece(&player->en_passant->piece, NoPiece, White); + chess_capture_piece(player, &player->en_passant->piece); player->en_passant = NULL; } @@ -540,13 +653,7 @@ static bool chess_valid_pawn_move(ChessState *state, Tile *from, Tile *to) return true; } - bool ret = to_piece.type != NoPiece && to_piece.colour != from->piece.colour; - - if (ret && (to->chess_coords.N == 1 || to->chess_coords.N == 8)) { // promote to queen - chess_set_piece(&from->piece, Queen, from->piece.colour); - } - - return ret; + return to_piece.type != NoPiece && to_piece.colour != from->piece.colour; } if (to_piece.type != NoPiece) { @@ -557,8 +664,6 @@ static bool chess_valid_pawn_move(ChessState *state, Tile *from, Tile *to) if (ret && n_diff == 2) { chess_pawn_en_passant_flag(state, to); - } else if (ret && (to->chess_coords.N == 1 || to->chess_coords.N == 8)) { - chess_set_piece(&from->piece, Queen, from->piece.colour); } return ret; @@ -664,14 +769,6 @@ static bool chess_valid_king_move(const Tile *from, const Tile *to) return true; } -/* - * Copies `piece_b` to `piece_a`. - */ -static void chess_copy_piece(Piece *piece_a, Piece *piece_b) -{ - memcpy(piece_a, piece_b, sizeof(Piece)); -} - static bool chess_piece_attacking_square(ChessState *state, ChessColour colour, Tile *to); /* @@ -692,12 +789,65 @@ static bool chess_player_in_check(ChessState *state, const Player *player) return false; } +/* + * Makes a mock move on the board and tests if it puts the player in check. + * + * Return true if move is valid. + * + * This function assumes that the legality of the move has already been checked. + */ +static bool chess_mock_move_valid(ChessState *state, const Player *player, Tile *from, Tile *to) +{ + Board *board = &state->board; + + bool in_check = false; + Tile *ep_tile = NULL; + Piece ep_piece; + + if (player->en_passant_move_number == -1) { // remove piece that was captured via en passant + ChessCoords ep_coords; + ep_coords.N = player->colour == White ? to->chess_coords.N - 1 : to->chess_coords.N + 1; + ep_coords.L = to->chess_coords.L; + ep_tile = chess_get_tile_at_chess_coords(board, &ep_coords); + + if (ep_tile == NULL) { + return false; + } + + chess_copy_piece(&ep_piece, &ep_tile->piece); + ep_tile->piece.type = NoPiece; + } + + Piece from_piece; + Piece to_piece; + chess_copy_piece(&from_piece, &from->piece); + chess_copy_piece(&to_piece, &to->piece); + + chess_copy_piece(&to->piece, &from->piece); + from->piece.type = NoPiece; + + if (chess_player_in_check(state, player)) { + in_check = true;; + } + + from->piece.type = from_piece.type; + chess_copy_piece(&to->piece, &to_piece); + + if (player->en_passant_move_number == -1) { + ep_tile->piece.type = ep_piece.type; + } + + return !in_check; +} + /* * Return 1 if we can legally move `from` to `to`. * Return 0 if move is legal but we're in check. * Return -1 if move is not legal. * * If `player` is null we don't check if move puts player in check. + * + * This function should not modify the game state. */ static int chess_valid_move(ChessState *state, const Player *player, Tile *from, Tile *to) { @@ -706,13 +856,11 @@ static int chess_valid_move(ChessState *state, const Player *player, Tile *from, } bool valid = false; - bool is_pawn = false; Board *board = &state->board; switch (from->piece.type) { case Pawn: - is_pawn = true; valid = chess_valid_pawn_move(state, from, to); break; @@ -743,28 +891,10 @@ static int chess_valid_move(ChessState *state, const Player *player, Tile *from, int ret = valid ? 1 : -1; - // make mock move and see if we're in check if (player != NULL && valid) { - Piece from_piece; - Piece to_piece; - chess_copy_piece(&from_piece, &from->piece); - chess_copy_piece(&to_piece, &to->piece); - - chess_copy_piece(&to->piece, &from->piece); - from->piece.type = NoPiece; - - if (chess_player_in_check(state, player)) { + if (!chess_mock_move_valid(state, player, from, to)) { ret = 0; } - - from->piece.type = from_piece.type; - chess_copy_piece(&to->piece, &to_piece); - - // if we promoted a pawn and we're in check we have to turn it back into a pawn - // TODO: Maybe validating functions shouldn't be allowed to modify the game state - if (ret == 0 && is_pawn && from->piece.type == Queen) { - chess_set_piece(&from->piece, Pawn, player->colour); - } } return ret; @@ -784,6 +914,8 @@ static bool chess_piece_attacking_square(ChessState *state, ChessColour colour, continue; } + // We only need to know if a piece has line of sight so we don't + // care if the move puts the player in check if (chess_valid_move(state, NULL, from, to) == 1) { return true; } @@ -827,7 +959,7 @@ static void chess_player_set_can_castle(Player *player, const Tile *tile) * * Return true if successfully castled. */ -static bool chess_player_castle(ChessState *state, Player *player, Tile *to) +static bool chess_player_castle(ChessState *state, Player *player, Tile *from, Tile *to) { if (!player->can_castle_ks && !player->can_castle_qs) { return false; @@ -835,20 +967,14 @@ static bool chess_player_castle(ChessState *state, Player *player, Tile *to) Board *board = &state->board; - Tile *holding_tile = player->holding_tile; - - if (holding_tile == NULL) { - return false; - } - - if (!(holding_tile->piece.type == King && to->piece.type == NoPiece)) { + if (!(from->piece.type == King && to->piece.type == NoPiece)) { return false; } int l_diff; int n_diff; - if (chess_get_chess_coords_diff(holding_tile, to, &l_diff, &n_diff) == -1) { + if (chess_get_chess_coords_diff(from, to, &l_diff, &n_diff) == -1) { return false; } @@ -928,16 +1054,16 @@ static bool chess_player_castle(ChessState *state, Player *player, Tile *to) // move king Piece old_king; chess_copy_piece(&old_king, &to->piece); - chess_copy_piece(&to->piece, &holding_tile->piece); + chess_copy_piece(&to->piece, &from->piece); - chess_set_piece(&holding_tile->piece, NoPiece, White); + chess_set_piece(&from->piece, NoPiece, White); player->holding_tile = NULL; if (chess_player_in_check(state, player)) { chess_copy_piece(&to->piece, &old_king); chess_set_piece(&rook_to_tile->piece, NoPiece, White); chess_set_piece(&rook_from_tile->piece, Rook, player->colour); - chess_set_piece(&holding_tile->piece, King, player->colour); + chess_set_piece(&from->piece, King, player->colour); return false; } @@ -947,71 +1073,11 @@ static bool chess_player_castle(ChessState *state, Player *player, Tile *to) return true; } -static void chess_capture_piece(Player *player, Piece *piece) +static void chess_update_state(ChessState *state, Player *self, Player *other, const Tile *from, const Tile *to) { - size_t idx = player->number_captured; + chess_player_check_en_passant(self); - if (idx < CHESS_SQUARES) { - chess_copy_piece(&player->captured[idx], piece); - ++player->number_captured; - } -} - -static void chess_try_move_piece(ChessState *state, Player *player) -{ - Tile *tile = chess_get_tile(state, state->curs_x, state->curs_y); - - if (tile == NULL) { - return; - } - - Tile *holding_tile = player->holding_tile; - - if (holding_tile == NULL) { - return; - } - - if (chess_chess_coords_overlap(&holding_tile->chess_coords, &tile->chess_coords)) { - state->message_length = 0; - player->holding_tile = NULL; - return; - } - - int valid = chess_valid_move(state, player, holding_tile, tile); - - if (valid != 1) { - if (!player->in_check && chess_player_castle(state, player, tile)) { - chess_set_piece(&holding_tile->piece, NoPiece, White); - goto on_success; - } - - player->holding_tile = NULL; - - const char *message = valid == -1 ? "Invalid move" : "Invalid move (check)"; - chess_set_status_message(state, message, strlen(message)); - return; - } - - Tile tmp_holding; - memcpy(&tmp_holding, holding_tile, sizeof(Tile)); - - if (tile->piece.type != NoPiece) { - chess_capture_piece(player, &tile->piece); - } - - chess_copy_piece(&tile->piece, &player->holding_tile->piece); - - player->holding_tile = NULL; - chess_set_piece(&holding_tile->piece, NoPiece, White); - - chess_player_set_can_castle(player, &tmp_holding); - -on_success: - chess_player_clear_en_passant(player); - - player->in_check = false; - - Player *other = chess_get_other_player(state); + self->in_check = false; if (chess_player_in_check(state, other)) { other->in_check = true; @@ -1023,10 +1089,151 @@ on_success: if (state->black_to_move) { ++state->move_number; } + + chess_print_move_notation(state, from, to, other->in_check); +} + +/* + * Attempts to make opponent's move. + * + * Return 0 on success. + * Return -1 on failure. + * + * This function shouldn't fail unless opponent is doing something fishy or there's a bug somewhere. + * On failure the game will abort and the player who made an invalid move will lose the game. + */ +static int chess_try_move_opponent(ChessState *state, Tile *from, Tile *to) +{ + Player *opponent = &state->other; + + if (!chess_player_to_move(state, opponent)) { + return -1; + } + + Tile from_orig; + memcpy(&from_orig, from, sizeof(Tile)); + + Tile to_orig; + memcpy(&to_orig, to, sizeof(Tile)); + + int valid = chess_valid_move(state, opponent, from, to); + + if (valid != 1) { + if (!opponent->in_check && chess_player_castle(state, opponent, from, to)) { + chess_set_piece(&from->piece, NoPiece, White); + goto on_success; + } + + return -1; + } + + if (to->piece.type != NoPiece) { + chess_capture_piece(opponent, &to->piece); + } + + chess_copy_piece(&to->piece, &from->piece); + chess_player_set_can_castle(opponent, &from_orig); + chess_set_piece(&from->piece, NoPiece, White); + + // check if we need to promote pawn to queen + if (from_orig.piece.type == Pawn) { + if (to->chess_coords.N == 1 || to->chess_coords.N == 8) { + chess_set_piece(&to->piece, Queen, to->piece.colour); + } + } + +on_success: + chess_update_state(state, opponent, &state->self, &from_orig, &to_orig); + return 0; +} + +static void chess_try_move_self(const GameData *game, ChessState *state, Player *self) +{ + if (!chess_player_to_move(state, self)) { + return; + } + + Tile *to_tile = chess_get_tile(state, state->curs_x, state->curs_y); + + if (to_tile == NULL) { + return; + } + + Tile *holding_tile = self->holding_tile; + + if (holding_tile == NULL) { + return; + } + + Tile from_orig; + memcpy(&from_orig, holding_tile, sizeof(Tile)); + + Tile to_orig; + memcpy(&to_orig, to_tile, sizeof(Tile)); + + if (chess_chess_coords_overlap(&holding_tile->chess_coords, &to_tile->chess_coords)) { + state->message_length = 0; + self->holding_tile = NULL; + return; + } + + int valid = chess_valid_move(state, self, holding_tile, to_tile); + + if (valid != 1) { + if (!self->in_check && chess_player_castle(state, self, holding_tile, to_tile)) { + if (chess_packet_send_move(game, &from_orig, to_tile) == -1) { + const char *message = "Connection error"; + chess_set_status_message(state, message, strlen(message)); + return; + } + + chess_set_piece(&holding_tile->piece, NoPiece, White); + + self->holding_tile = NULL; + + goto on_success; + } + + self->holding_tile = NULL; + + const char *message = valid == -1 ? "Invalid move" : "Invalid move (check)"; + chess_set_status_message(state, message, strlen(message)); + return; + } + + if (chess_packet_send_move(game, &from_orig, to_tile) == -1) { + const char *message = "Failed to move: Connection error"; + chess_set_status_message(state, message, strlen(message)); + return; + } + + if (to_tile->piece.type != NoPiece) { + chess_capture_piece(self, &to_tile->piece); + } + + chess_copy_piece(&to_tile->piece, &self->holding_tile->piece); + + self->holding_tile = NULL; + chess_set_piece(&holding_tile->piece, NoPiece, White); + chess_player_set_can_castle(self, &from_orig); + + // check if we need to promote pawn to queen + if (from_orig.piece.type == Pawn) { + if (to_tile->chess_coords.N == 1 || to_tile->chess_coords.N == 8) { + chess_set_piece(&to_tile->piece, Queen, to_tile->piece.colour); + } + } + +on_success: + chess_update_state(state, self, &state->other, &from_orig, &to_orig); } static void chess_pick_up_piece(ChessState *state, Player *player) { + if (!chess_player_to_move(state, player)) { + return; + } + Tile *tile = chess_get_tile(state, state->curs_x, state->curs_y); if (tile == NULL) { @@ -1160,9 +1367,8 @@ static bool chess_game_is_statemate(ChessState *state) */ static bool chess_game_checkmate(ChessState *state) { - const Player *self = chess_get_player_to_move(state); - - return !chess_any_piece_can_move(state, self); + const Player *player = chess_get_player_to_move(state); + return !chess_any_piece_can_move(state, player); } /* @@ -1172,7 +1378,7 @@ static void chess_update_status(ChessState *state) { if (chess_game_is_statemate(state)) { state->status = Stalemate; - const char *message = "Game over: Stalemate"; + const char *message = "Stalemate"; chess_set_status_message(state, message, strlen(message)); return; } @@ -1185,18 +1391,18 @@ static void chess_update_status(ChessState *state) } } -static void chess_do_input(ChessState *state) +static void chess_do_input(const GameData *game, ChessState *state) { - if (state->status == Checkmate || state->status == Stalemate) { + if (state->status != Playing) { return; } - Player *player = chess_get_player_to_move(state); + Player *self = &state->self; - if (player->holding_tile == NULL) { - chess_pick_up_piece(state, player); + if (self->holding_tile == NULL) { + chess_pick_up_piece(state, self); } else { - chess_try_move_piece(state, player); + chess_try_move_self(game, state, self); chess_update_status(state); } } @@ -1270,7 +1476,7 @@ static int chess_get_display_colour(ChessColour p_colour, int tile_colour) } } -static void chess_draw_board_white(WINDOW *win, const Board *board) +static void chess_draw_board_coords_white(WINDOW *win, const Board *board) { for (size_t i = 0; i < CHESS_BOARD_COLUMNS; ++i) { mvwaddch(win, board->y_bottom_bound, board->x_left_bound + 1 + (i * CHESS_TILE_SIZE_X), Board_Letters[i]); @@ -1281,18 +1487,18 @@ static void chess_draw_board_white(WINDOW *win, const Board *board) } } -static void chess_draw_board_black(WINDOW *win, const Board *board) +static void chess_draw_board_coords_black(WINDOW *win, const Board *board) { - size_t l_idx = CHESS_NUM_BOARD_LETTERS - 1; + int l_idx = CHESS_NUM_BOARD_LETTERS - 1; for (size_t i = 0; i < CHESS_BOARD_COLUMNS && l_idx >= 0; ++i, --l_idx) { mvwaddch(win, board->y_bottom_bound, board->x_left_bound + 1 + (i * CHESS_TILE_SIZE_X), Board_Letters[l_idx]); } - size_t n_idx = CHESS_BOARD_ROWS; + int n_idx = CHESS_BOARD_ROWS; for (size_t i = 0; i < CHESS_BOARD_ROWS && n_idx > 0; ++i, --n_idx) { - mvwprintw(win, board->y_bottom_bound - 1 - (i * CHESS_TILE_SIZE_Y), board->x_left_bound - 1, "%zu", n_idx); + mvwprintw(win, board->y_bottom_bound - 1 - (i * CHESS_TILE_SIZE_Y), board->x_left_bound - 1, "%d", n_idx); } } @@ -1325,17 +1531,17 @@ static void chess_draw_board(WINDOW *win, ChessState *state) if (piece.type != NoPiece) { int colour = chess_get_display_colour(piece.colour, tile.colour); - wattron(win, COLOR_PAIR(colour)); + wattron(win, A_BOLD | COLOR_PAIR(colour)); mvwaddch(win, tile.coords.y, tile.coords.x + 1, piece.display_char); - wattroff(win, COLOR_PAIR(colour)); + wattroff(win, A_BOLD | COLOR_PAIR(colour)); } } if (state->self.colour == White) { - chess_draw_board_white(win, board); + chess_draw_board_coords_white(win, board); } else { - chess_draw_board_black(win, board); + chess_draw_board_coords_black(win, board); } // if holding a piece draw it at cursor position @@ -1357,8 +1563,47 @@ static void chess_print_status(WINDOW *win, ChessState *state) const Player *player = chess_get_player_to_move(state); char message[CHESS_MAX_MESSAGE_SIZE + 1]; - snprintf(message, sizeof(message), "%s to move %s", state->black_to_move ? "Black" : "White", - player->in_check ? "(check)" : ""); + + switch (state->status) { + case Playing: { + snprintf(message, sizeof(message), "%s to move %s", state->black_to_move ? "Black" : "White", + player->in_check ? "(check)" : ""); + break; + } + + case Initializing: { + snprintf(message, sizeof(message), "Waiting for opponent to connect"); + break; + } + + case Resigned: { + snprintf(message, sizeof(message), "Opponent resigned"); + break; + } + + case Stalemate: + + /* fallthrough */ + case Checkmate: { + const char *score_str = NULL; + + if (state->self.in_check) { + score_str = state->self.colour == White ? "0 - 1" : "1 - 0"; + } else if (state->other.in_check) { + score_str = state->other.colour == White ? "0 - 1" : "1 - 0"; + } else { + score_str = "1/2 - 1/2"; + } + + snprintf(message, sizeof(message), "%s", score_str); + break; + } + + default: { + snprintf(message, sizeof(message), "Invalid game state"); + break; + } + } int x_mid = (board->x_left_bound + (CHESS_TILE_SIZE_X * (CHESS_BOARD_COLUMNS / 2))) - (strlen(message) / 2); mvwprintw(win, board->y_top_bound - 2, x_mid, message); @@ -1378,16 +1623,29 @@ static void chess_print_captured(const GameData *game, WINDOW *win, ChessState * const Player *self = &state->self; const Player *other = &state->other; - int self_top_y_start = board->y_bottom_bound - (CHESS_TILE_SIZE_Y * 3); - int other_top_y_start = board->y_top_bound; + const int score_diff = self->score - other->score; + + const int self_top_y_start = board->y_bottom_bound - (CHESS_TILE_SIZE_Y * 3) + 1; + const int other_top_y_start = board->y_top_bound + 1; const int left_x_start = board->x_right_bound + 1; const int right_x_border = game_x_right_bound(game) - 1; size_t idx = 0; - int colour = self->colour == White ? YELLOW : WHITE; - wattron(win, COLOR_PAIR(colour)); + int self_colour = self->colour == White ? WHITE : YELLOW; + int other_colour = self_colour == YELLOW ? WHITE : YELLOW; + + wattron(win, A_BOLD); + + if (score_diff > 0) { + wattron(win, COLOR_PAIR(self_colour)); + mvwprintw(win, self_top_y_start - 1, left_x_start, "+%d", score_diff); + wattroff(win, COLOR_PAIR(self_colour)); + } + + + wattron(win, COLOR_PAIR(other_colour)); for (size_t y = self_top_y_start; y < board->y_bottom_bound; ++y) { for (size_t x = left_x_start; x < right_x_border && idx < self->number_captured; x += 2, ++idx) { @@ -1396,12 +1654,17 @@ static void chess_print_captured(const GameData *game, WINDOW *win, ChessState * } } - wattroff(win, COLOR_PAIR(colour)); + wattroff(win, COLOR_PAIR(other_colour)); - colour = colour == YELLOW ? WHITE : YELLOW; idx = 0; - wattron(win, COLOR_PAIR(colour)); + if (score_diff < 0) { + wattron(win, COLOR_PAIR(other_colour)); + mvwprintw(win, other_top_y_start - 1, left_x_start, "+%d", abs(score_diff)); + wattroff(win, COLOR_PAIR(other_colour)); + } + + wattron(win, COLOR_PAIR(self_colour)); for (size_t y = other_top_y_start; y < board->y_bottom_bound; ++y) { for (size_t x = left_x_start; x < right_x_border && idx < other->number_captured; x += 2, ++idx) { @@ -1410,7 +1673,7 @@ static void chess_print_captured(const GameData *game, WINDOW *win, ChessState * } } - wattroff(win, COLOR_PAIR(colour)); + wattroff(win, A_BOLD | COLOR_PAIR(self_colour)); } static void chess_draw_interface(const GameData *game, WINDOW *win, ChessState *state) @@ -1466,7 +1729,7 @@ void chess_cb_on_keypress(GameData *game, int key, void *cb_data) } case '\r': { - chess_do_input(state); + chess_do_input(game, state); break; } @@ -1486,8 +1749,114 @@ void chess_cb_kill(GameData *game, void *cb_data) free(state); + chess_packet_send_resign(game); + game_set_cb_render_window(game, NULL, NULL); game_set_cb_kill(game, NULL, NULL); + game_set_cb_on_keypress(game, NULL, NULL); + game_set_cb_on_packet(game, NULL, NULL); +} + +/* + * Attempts to handle opponent's move. + * + * Return 0 on success. + * Return -1 on failure. + */ +#define CHESS_PACKET_MOVE_SIZE 4 +static int chess_handle_opponent_move_packet(const GameData *game, ChessState *state, const uint8_t *data, + size_t length) +{ + if (length != CHESS_PACKET_MOVE_SIZE || data == NULL) { + return -1; + } + + char from_l = data[0]; + uint8_t from_n = data[1]; + char to_l = data[2]; + uint8_t to_n = data[3]; + + ChessCoords from_coords = (ChessCoords) { + from_l, from_n, + }; + + ChessCoords to_coords = (ChessCoords) { + to_l, to_n, + }; + + Board *board = &state->board; + + Tile *from_tile = chess_get_tile_at_chess_coords(board, &from_coords); + + if (from_tile == NULL) { + return -1; + } + + Tile *to_tile = chess_get_tile_at_chess_coords(board, &to_coords); + + if (to_tile == NULL) { + return -1; + } + + if (chess_try_move_opponent(state, from_tile, to_tile) != 0) { + fprintf(stderr, "opponent tried to make an illegal move: %c%d-%c%d\n", from_l, from_n, to_l, to_n); + return -1; + } + + return 0; + +} + +static void chess_cb_on_packet(GameData *game, const uint8_t *data, size_t length, void *cb_data) +{ + if (length == 0 || data == NULL) { + return; + } + + if (!cb_data) { + return; + } + + ChessState *state = (ChessState *)cb_data; + + ChessPacketType type = data[0]; + + switch (type) { + case CHESS_PACKET_INIT_ACCEPT_INVITE: { + if (state->status == Initializing) { + state->status = Playing; + } + + break; + } + + case CHESS_PACKET_RESIGN: { + if (state->status == Playing) { + state->status = Resigned; + } + + break; + } + + case CHESS_PACKET_MOVE_PIECE: { + if (state->status == Playing) { + int ret = chess_handle_opponent_move_packet(game, state, data + 1, length - 1); + + if (ret != 0) { + state->status = Resigned; + } + + chess_update_status(state); + } + + break; + } + + default: { + fprintf(stderr, "Got unknown chess packet type: %d\n", type); + break; + } + } } static int chess_init_board(GameData *game, ChessState *state, bool self_is_white) @@ -1594,7 +1963,60 @@ static int chess_init_board(GameData *game, ChessState *state, bool self_is_whit return 0; } -int chess_initialize(GameData *game) +static int chess_packet_send_resign(const GameData *game) +{ + uint8_t data[1]; + data[0] = CHESS_PACKET_RESIGN; + + if (game_send_packet(game, data, 1, GP_Data) == -1) { + return -1; + } + + return 0; +} + +static int chess_packet_send_move(const GameData *game, const Tile *from, const Tile *to) +{ + uint8_t data[5]; + data[0] = CHESS_PACKET_MOVE_PIECE; + data[1] = from->chess_coords.L; + data[2] = from->chess_coords.N; + data[3] = to->chess_coords.L; + data[4] = to->chess_coords.N; + + if (game_send_packet(game, data, 5, GP_Data) == -1) { + return -1; + } + + return 0; +} + +static int chess_packet_send_invite(const GameData *game, bool self_is_white) +{ + uint8_t data[2]; + data[0] = CHESS_PACKET_INIT_SEND_INVITE; + data[1] = self_is_white ? Black : White; + + if (game_send_packet(game, data, 2, GP_Invite) == -1) { + return -1; + } + + return 0; +} + +static int chess_packet_send_accept(const GameData *game) +{ + uint8_t data[1]; + data[0] = CHESS_PACKET_INIT_ACCEPT_INVITE; + + if (game_send_packet(game, data, 1, GP_Data) == -1) { + return -1; + } + + return 0; +} + +int chess_initialize(GameData *game, const uint8_t *init_data, size_t length) { if (game_set_window_shape(game, GW_ShapeSquare) == -1) { return -1; @@ -1603,16 +2025,37 @@ int chess_initialize(GameData *game) ChessState *state = calloc(1, sizeof(ChessState)); if (state == NULL) { - return -1; + return -3; + } + + bool self_is_host = false; + bool self_is_white = false; + + if (length == 0) { + self_is_host = true; + self_is_white = rand() % 2 == 0; + } else { + if (length < 2 || init_data[0] != CHESS_PACKET_INIT_SEND_INVITE) { + free(state); + return -2; + } + + ChessColour colour = (ChessColour)init_data[1]; + + if (colour != White && colour != Black) { + free(state); + return -2; + } + + self_is_white = colour == White; } - bool self_is_white = rand() % 2 == 0; state->self.colour = self_is_white ? White : Black; state->other.colour = !self_is_white ? White : Black; if (chess_init_board(game, state, self_is_white) == -1) { free(state); - return -1; + return -3; } state->self.can_castle_ks = true; @@ -1620,11 +2063,24 @@ int chess_initialize(GameData *game) state->other.can_castle_ks = true; state->other.can_castle_qs = true; - game_set_update_interval(game, 20); + if (self_is_host) { + if (chess_packet_send_invite(game, self_is_white) == -1) { + free(state); + return -2; + } + } else { + if (chess_packet_send_accept(game) == -1) { + free(state); + return -2; + } + + state->status = Playing; + } game_set_cb_render_window(game, chess_cb_render_window, state); game_set_cb_on_keypress(game, chess_cb_on_keypress, state); game_set_cb_kill(game, chess_cb_kill, state); + game_set_cb_on_packet(game, chess_cb_on_packet, state); return 0; } diff --git a/src/game_chess.h b/src/game_chess.h index 1ec8fec..f7e38e4 100644 --- a/src/game_chess.h +++ b/src/game_chess.h @@ -25,6 +25,20 @@ #include "game_base.h" -int chess_initialize(GameData *game); +/* + * Initializes chess game state. + * + * If `init_data` is non-null, this indicates that we were invited to the game. + * + * If we're the inviter, we send an invite packet after initialization. If we're the + * invitee, we send a handshake response packet to the inviter. + * + * Return 0 on success. + * Return -1 if window is too small. + * Return -2 on network related error. + * Return -3 on other error. + */ +int chess_initialize(GameData *game, const uint8_t *init_data, size_t length); #endif // GAME_CHESS + diff --git a/src/game_util.c b/src/game_util.c index 045837f..094a412 100644 --- a/src/game_util.c +++ b/src/game_util.c @@ -22,6 +22,7 @@ #include #include +#include #include "game_util.h" #include "toxic.h" @@ -156,3 +157,38 @@ int game_util_random_colour(void) return RED; } } + +static size_t net_pack_u16(uint8_t *bytes, uint16_t v) +{ + bytes[0] = (v >> 8) & 0xff; + bytes[1] = v & 0xff; + return sizeof(v); +} + +static size_t net_unpack_u16(const uint8_t *bytes, uint16_t *v) +{ + uint8_t hi = bytes[0]; + uint8_t lo = bytes[1]; + *v = ((uint16_t)hi << 8) | lo; + return sizeof(*v); +} + +size_t game_util_pack_u32(uint8_t *bytes, uint32_t v) +{ + uint8_t *p = bytes; + p += net_pack_u16(p, (v >> 16) & 0xffff); + p += net_pack_u16(p, v & 0xffff); + return p - bytes; +} + +size_t game_util_unpack_u32(const uint8_t *bytes, uint32_t *v) +{ + const uint8_t *p = bytes; + uint16_t hi; + uint16_t lo; + p += net_unpack_u16(p, &hi); + p += net_unpack_u16(p, &lo); + *v = ((uint32_t)hi << 16) | lo; + return p - bytes; +} + diff --git a/src/game_util.h b/src/game_util.h index 44b30e6..36e5716 100644 --- a/src/game_util.h +++ b/src/game_util.h @@ -89,4 +89,15 @@ void game_util_move_coords(Direction direction, Coords *coords); */ int game_util_random_colour(void); +/* + * Packs an unsigned 32 bit integer `v` into `bytes`. + */ +size_t game_util_pack_u32(uint8_t *bytes, uint32_t v); + +/* + * Unpacks an unsigned 32 bit integer in `bytes` to `v`. + */ +size_t game_util_unpack_u32(const uint8_t *bytes, uint32_t *v); + #endif // GAME_UTIL + diff --git a/src/global_commands.c b/src/global_commands.c index 7622079..a944b1e 100644 --- a/src/global_commands.c +++ b/src/global_commands.c @@ -373,7 +373,8 @@ void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MA } } - int ret = game_initialize(self, m, type, force_small); + uint32_t id = rand(); + int ret = game_initialize(self, m, type, id, NULL, 0, force_small); switch (ret) { case 0: { @@ -386,6 +387,18 @@ void cmd_game(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MA return; } + case -2: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize: Network error."); + return; + + } + + case -3: { + line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, + "Game is multiplayer only. Try the command again in the chat window of the contact you wish to play with."); + return; + } + default: { line_info_add(self, false, NULL, NULL, SYS_MSG, 0, 0, "Game failed to initialize (error %d)", ret); return; diff --git a/src/toxic.c b/src/toxic.c index 99dd0bd..28409b0 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -806,6 +806,7 @@ static void init_tox_callbacks(Tox *m) tox_callback_file_chunk_request(m, on_file_chunk_request); tox_callback_file_recv_control(m, on_file_recv_control); tox_callback_file_recv_chunk(m, on_file_recv_chunk); + tox_callback_friend_lossless_packet(m, on_lossless_custom_packet); } static void init_tox_options(struct Tox_Options *tox_opts) diff --git a/src/toxic.h b/src/toxic.h index ff871e2..e4615f7 100644 --- a/src/toxic.h +++ b/src/toxic.h @@ -135,5 +135,7 @@ void on_file_recv(Tox *m, uint32_t friendnumber, uint32_t filenumber, uint32_t k const uint8_t *filename, size_t filename_length, void *userdata); void on_friend_typing(Tox *m, uint32_t friendnumber, bool is_typing, void *userdata); void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, void *userdata); +void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length, void *userdata); + #endif /* TOXIC_H */ diff --git a/src/windows.c b/src/windows.c index bbcc161..f0c2790 100644 --- a/src/windows.c +++ b/src/windows.c @@ -323,6 +323,49 @@ void on_friend_read_receipt(Tox *m, uint32_t friendnumber, uint32_t receipt, voi } } } + +void on_lossless_custom_packet(Tox *m, uint32_t friendnumber, const uint8_t *data, size_t length, void *userdata) +{ + UNUSED_VAR(userdata); + + if (length == 0 || data == NULL) { + return; + } + + uint8_t type = data[0]; + + switch (type) { + case CUSTOM_PACKET_GAME_INVITE: { + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + ToxWindow *window = windows[i]; + + if (window != NULL && window->onGameInvite != NULL) { + window->onGameInvite(window, m, friendnumber, data + 1, length - 1); + } + } + + break; + } + + case CUSTOM_PACKET_GAME_DATA: { + for (size_t i = 0; i < MAX_WINDOWS_NUM; ++i) { + ToxWindow *window = windows[i]; + + if (window != NULL && window->onGameData != NULL) { + window->onGameData(window, m, friendnumber, data + 1, length - 1); + } + } + + break; + } + + default: { + fprintf(stderr, "Got unknown custom packet of type: %u\n", type); + return; + } + } +} + /* CALLBACKS END */ int add_window(Tox *m, ToxWindow *w) diff --git a/src/windows.h b/src/windows.h index b5bbb5a..fc97f24 100644 --- a/src/windows.h +++ b/src/windows.h @@ -43,6 +43,13 @@ #define TOP_BAR_HEIGHT 1 #define WINDOW_BAR_HEIGHT 1 + +typedef enum CustomPacket { + CUSTOM_PACKET_GAME_INVITE = 160, + CUSTOM_PACKET_GAME_DATA = 161, +} CustomPacket; + + /* ncurses colour pairs as FOREGROUND_BACKGROUND. No background defaults to black. */ typedef enum { WHITE, @@ -160,6 +167,10 @@ struct ToxWindow { void(*onTypingChange)(ToxWindow *, Tox *, uint32_t, bool); void(*onReadReceipt)(ToxWindow *, Tox *, uint32_t, uint32_t); + /* custom packets/games */ + void(*onGameInvite)(ToxWindow *, Tox *, uint32_t, const uint8_t *, size_t); + void(*onGameData)(ToxWindow *, Tox *, uint32_t, const uint8_t *, size_t); + #ifdef AUDIO void(*onInvite)(ToxWindow *, ToxAV *, uint32_t, int);