From 9a0c07e53cd8aeb4480e20dfcf1ae347d9ba584d Mon Sep 17 00:00:00 2001 From: MysterD Date: Mon, 14 Sep 2020 22:05:20 -0700 Subject: [PATCH] Added in-game chat Fixes #35 --- src/game/chat.c | 141 ++++++++++++++++++++++++ src/game/chat.h | 8 ++ src/game/ingame_menu.c | 51 ++++++--- src/game/ingame_menu.h | 3 + src/menu/custom_menu.c | 2 +- src/pc/controller/controller_keyboard.c | 14 ++- src/pc/controller/controller_keyboard.h | 4 +- src/pc/network/discord/discord.c | 6 +- src/pc/network/network.c | 1 + src/pc/network/network.h | 5 + src/pc/network/packets/packet_chat.c | 51 +++++++++ 11 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 src/game/chat.c create mode 100644 src/game/chat.h create mode 100644 src/pc/network/packets/packet_chat.c diff --git a/src/game/chat.c b/src/game/chat.c new file mode 100644 index 00000000..dd04f991 --- /dev/null +++ b/src/game/chat.c @@ -0,0 +1,141 @@ +#include +#include +#include + +#include "chat.h" +#include "game_init.h" +#include "ingame_menu.h" +#include "segment2.h" +#include "gfx_dimensions.h" +#include "config.h" +#include "PR/gbi.h" +#include "pc/controller/controller_keyboard.h" +#include "pc/network/network.h" +#include "audio_defines.h" +#include "audio/external.h" + +#define CHAT_DIALOG_MAX 96 +#define CHAT_MESSAGES_MAX 16 +#define CHAT_LIFE_MAX 400 + +struct ChatMessage { + u8 dialog[CHAT_DIALOG_MAX]; + u8 isLocal; + u16 life; +}; + +static char inputMessage[CHAT_DIALOG_MAX] = { 0 }; +static struct ChatMessage message[CHAT_MESSAGES_MAX] = { 0 }; +static u8 onMessageIndex = 0; +static u8 sInChatInput = FALSE; + +#define CHAT_SCALE 0.5f +#define CHAT_SPACE 10.0f +#define CHATBOX_PAD_X 0.0215f +#define CHATBOX_SCALE_X 0.00385f +#define CHATBOX_SCALE_Y 0.115f +#define CHATBOX_X 2.0f +#define CHATBOX_Y 11.0f +#define CHAT_X 4.0f +#define CHAT_Y -18.0f + +static void render_chat_message(struct ChatMessage* chatMessage, u8 index) { + f32 textWidth = get_generic_dialog_width(chatMessage->dialog); + f32 alphaScale = ((f32)chatMessage->life / (f32)(CHAT_LIFE_MAX / 20.0f)); + alphaScale *= alphaScale; + if (alphaScale > 1) { alphaScale = 1; } + + f32 chatBoxWidth = CHATBOX_SCALE_X * textWidth + CHATBOX_PAD_X; + create_dl_translation_matrix(MENU_MTX_PUSH, GFX_DIMENSIONS_FROM_LEFT_EDGE(CHATBOX_X), CHATBOX_Y + index * CHAT_SPACE, 0); + create_dl_scale_matrix(MENU_MTX_NOPUSH, chatBoxWidth, CHATBOX_SCALE_Y, 1.0f); + + u8 boxR, boxG, boxB; + if (chatMessage->isLocal == 2) { + boxR = 150; + boxG = 150; + boxB = 255; + } else { + f32 rgbScale = (((f32)chatMessage->life - ((f32)CHAT_LIFE_MAX * 0.98f)) / ((f32)CHAT_LIFE_MAX * 0.02f)); + if (chatMessage->isLocal || rgbScale < 0) { rgbScale = 0; } + boxR = 255 * rgbScale; + boxG = 255 * rgbScale; + boxB = 255 * rgbScale; + } + + gDPSetEnvColor(gDisplayListHead++, boxR, boxG, boxB, 110 * alphaScale); + gSPDisplayList(gDisplayListHead++, dl_draw_text_bg_box); + + create_dl_scale_matrix(MENU_MTX_NOPUSH, CHAT_SCALE / chatBoxWidth, CHAT_SCALE / CHATBOX_SCALE_Y, 1.0f); + + u8 textR, textG, textB; + if (chatMessage->isLocal == 1) { + textR = 200; + textG = 200; + textB = 255; + } else if (chatMessage->isLocal == 2) { + textR = 0; + textG = 0; + textB = 0; + } else { + textR = 255; + textG = 255; + textB = 255; + } + + gSPDisplayList(gDisplayListHead++, dl_ia_text_begin); + gDPSetEnvColor(gDisplayListHead++, textR, textG, textB, 255 * alphaScale); + print_generic_string(CHAT_X, CHAT_Y, chatMessage->dialog); + gSPDisplayList(gDisplayListHead++, dl_ia_text_end); + + gSPPopMatrix(gDisplayListHead++, G_MTX_MODELVIEW); +} + +void chat_add_message(char* ascii, u8 isLocal) { + message[onMessageIndex].dialog[0] = isLocal ? 0xFD : 0xFA; + message[onMessageIndex].dialog[1] = 0x9E; + str_ascii_to_dialog(ascii, &message[onMessageIndex].dialog[2], MIN(strlen(ascii), CHAT_DIALOG_MAX - 3)); + message[onMessageIndex].life = CHAT_LIFE_MAX; + message[onMessageIndex].isLocal = isLocal ? 1 : 0; + onMessageIndex = (onMessageIndex + 1) % CHAT_MESSAGES_MAX; + play_sound(isLocal ? SOUND_MENU_MESSAGE_DISAPPEAR : SOUND_MENU_MESSAGE_APPEAR, gDefaultSoundArgs); +} + +static void chat_stop_input(void) { + sInChatInput = FALSE; + keyboard_stop_text_input(); +} + +static void chat_send_input(void) { + sInChatInput = FALSE; + keyboard_stop_text_input(); + if (strlen(gTextInput) == 0) { return; } + chat_add_message(gTextInput, TRUE); + network_send_chat(gTextInput); +} + +void chat_start_input(void) { + sInChatInput = TRUE; + keyboard_start_text_input(TIM_SINGLE_LINE, CHAT_DIALOG_MAX - 3, chat_stop_input, chat_send_input); +} + + +void render_chat(void) { + u8 count = 0; + if (sInChatInput) { + struct ChatMessage inputMessage = { 0 }; + inputMessage.dialog[0] = 0xFD; + inputMessage.dialog[1] = 0x9E; + str_ascii_to_dialog(gTextInput, &inputMessage.dialog[2], MIN(strlen(gTextInput), CHAT_DIALOG_MAX - 3)); + inputMessage.isLocal = 2; + inputMessage.life = CHAT_LIFE_MAX; + render_chat_message(&inputMessage, count++); + } + + u8 index = onMessageIndex; + for (int i = 0; i < CHAT_MESSAGES_MAX; i++) { + if (--index >= CHAT_MESSAGES_MAX) { index = CHAT_MESSAGES_MAX - 1; } + if (message[index].life == 0) { continue; } + render_chat_message(&message[index], count++); + message[index].life--; + } +} diff --git a/src/game/chat.h b/src/game/chat.h new file mode 100644 index 00000000..caa105b6 --- /dev/null +++ b/src/game/chat.h @@ -0,0 +1,8 @@ +#ifndef CHAT_H +#define CHAT_H + +void render_chat(void); +void chat_add_message(char* ascii, u8 isLocal); +void chat_start_input(void); + +#endif \ No newline at end of file diff --git a/src/game/ingame_menu.c b/src/game/ingame_menu.c index 8a77cc5a..bc8903ab 100644 --- a/src/game/ingame_menu.c +++ b/src/game/ingame_menu.c @@ -31,6 +31,7 @@ #ifdef EXT_OPTIONS_MENU #include "options_menu.h" #endif +#include "chat.h" u16 gDialogColorFadeTimer; s8 gLastDialogLineNum; @@ -415,27 +416,43 @@ void render_multi_text_string(s16 *xPos, s16 *yPos, s8 multiTextID) void str_ascii_to_dialog(const char* string, u8* dialog, u16 length) { for (int i = 0; i < length; i++) { switch (string[i]) { - case '\'': dialog[i] = 0x3E; break; - case '.': dialog[i] = 0x3F; break; - case ',': dialog[i] = DIALOG_CHAR_COMMA; break; - case '-': dialog[i] = 0x9F; break; - case '(': dialog[i] = 0xE1; break; - case ')': dialog[i] = 0xE3; break; - case '&': dialog[i] = 0xE5; break; - case '!': dialog[i] = 0xF2; break; - case '%': dialog[i] = 0xF3; break; - case '?': dialog[i] = 0xF4; break; - case '"': dialog[i] = 0xF6; break; // 0xF5 is opening quote - case '~': dialog[i] = 0xF7; break; - case '*': dialog[i] = 0xFB; break; - case ' ': dialog[i] = DIALOG_CHAR_SPACE; break; - case '\n': dialog[i] = DIALOG_CHAR_NEWLINE; break; - default: dialog[i] = ASCII_TO_DIALOG(string[i]); + case '\'': dialog[i] = 0x3E; break; + case '.': dialog[i] = 0x3F; break; + case ',': dialog[i] = DIALOG_CHAR_COMMA; break; + case '-': dialog[i] = 0x9F; break; + case '(': dialog[i] = 0xE1; break; + case ')': dialog[i] = 0xE3; break; + case '&': dialog[i] = 0xE5; break; + case '!': dialog[i] = 0xF2; break; + case '%': dialog[i] = 0xF3; break; + case '?': dialog[i] = 0xF4; break; + case '"': dialog[i] = 0xF6; break; // 0xF5 is opening quote + case '~': dialog[i] = 0xF7; break; + case '*': dialog[i] = 0xFB; break; + case ' ': dialog[i] = DIALOG_CHAR_SPACE; break; + case '\n': dialog[i] = DIALOG_CHAR_NEWLINE; break; + default: dialog[i] = ((u8)string[i] < 0xF0) ? ASCII_TO_DIALOG(string[i]) : string[i]; } } dialog[length] = DIALOG_CHAR_TERMINATOR; } +f32 get_generic_dialog_width(u8* dialog) { + f32 width = 0; + u8* d = dialog; + while (*d != DIALOG_CHAR_TERMINATOR) { + width += (f32)(gDialogCharWidths[*d]); + d++; + } + return width; +} + +f32 get_generic_ascii_string_width(const char* ascii) { + u8 dialog[256] = { DIALOG_CHAR_TERMINATOR }; + str_ascii_to_dialog(ascii, dialog, strlen(ascii)); + return get_generic_dialog_width(dialog); +} + void print_generic_ascii_string(s16 x, s16 y, const char* ascii) { u8 dialog[256] = { DIALOG_CHAR_TERMINATOR }; str_ascii_to_dialog(ascii, dialog, strlen(ascii)); @@ -3178,6 +3195,8 @@ s16 render_menus_and_dialogs() { create_dl_ortho_matrix(); + render_chat(); + if (gMenuMode != -1) { switch (gMenuMode) { case 0: diff --git a/src/game/ingame_menu.h b/src/game/ingame_menu.h index 958919e0..15985b5e 100644 --- a/src/game/ingame_menu.h +++ b/src/game/ingame_menu.h @@ -117,6 +117,8 @@ void create_dl_identity_matrix(void); void create_dl_translation_matrix(s8 pushOp, f32 x, f32 y, f32 z); void create_dl_ortho_matrix(void); void str_ascii_to_dialog(const char* string, u8* dialog, u16 length); +f32 get_generic_dialog_width(u8* dialog); +f32 get_generic_ascii_string_width(const char* ascii); void print_generic_ascii_string(s16 x, s16 y, const char* ascii); void print_generic_string(s16 x, s16 y, const u8 *str); void print_hud_lut_string(s8 hudLUT, s16 x, s16 y, const u8 *str); @@ -150,5 +152,6 @@ void render_hud_cannon_reticle(void); void reset_red_coins_collected(void); s16 render_menus_and_dialogs(void); s16 render_sync_level_screen(void); +void create_dl_scale_matrix(s8 pushOp, f32 x, f32 y, f32 z); #endif // INGAME_MENU_H diff --git a/src/menu/custom_menu.c b/src/menu/custom_menu.c index c388271e..305d7258 100644 --- a/src/menu/custom_menu.c +++ b/src/menu/custom_menu.c @@ -178,7 +178,7 @@ static void connect_menu_on_connection_attempt(void) { static void connect_menu_on_click(void) { gConnectionJoinError[0] = '\0'; - keyboard_start_text_input(TIM_IP, custom_menu_close, connect_menu_on_connection_attempt); + keyboard_start_text_input(TIM_IP, MAX_TEXT_INPUT, custom_menu_close, connect_menu_on_connection_attempt); // fill in our last attempt if (configJoinPort == 0) { configJoinPort = DEFAULT_PORT; } diff --git a/src/pc/controller/controller_keyboard.c b/src/pc/controller/controller_keyboard.c index 8536a62c..ed8320f0 100644 --- a/src/pc/controller/controller_keyboard.c +++ b/src/pc/controller/controller_keyboard.c @@ -15,6 +15,8 @@ #include "pc/gfx/gfx_window_manager_api.h" #include "pc/pc_main.h" #include "engine/math_util.h" +#include "menu/file_select.h" +#include "game/chat.h" // TODO: use some common lookup header #define SCANCODE_BACKSPACE 0x0E @@ -39,6 +41,7 @@ static u32 keyboard_lastkey = VK_INVALID; char gTextInput[MAX_TEXT_INPUT]; static bool inTextInput = false; +static u8 maxTextInput = 0; u8 held_ctrl, held_shift, held_alt; static enum TextInputMode textInputMode; @@ -110,6 +113,13 @@ bool keyboard_on_key_down(int scancode) { return FALSE; } + if (scancode == SCANCODE_ENTER) { + if (sSelectedFileNum != 0) { + chat_start_input(); + return FALSE; + } + } + int mapped = keyboard_map_scancode(scancode); keyboard_buttons_down |= mapped; keyboard_lastkey = scancode; @@ -136,10 +146,11 @@ void keyboard_on_all_keys_up(void) { keyboard_buttons_down = 0; } -char* keyboard_start_text_input(enum TextInputMode inInputMode, void (*onEscape)(void), void (*onEnter)(void)) { +char* keyboard_start_text_input(enum TextInputMode inInputMode, u8 inMaxTextInput, void (*onEscape)(void), void (*onEnter)(void)) { // set text-input events textInputOnEscape = onEscape; textInputOnEnter = onEnter; + maxTextInput = inMaxTextInput; // clear buffer for (int i = 0; i < MAX_TEXT_INPUT; i++) { gTextInput[i] = '\0'; } @@ -202,6 +213,7 @@ void keyboard_on_text_input(char* text) { while (*text != '\0') { // make sure we don't overrun the buffer if (i >= MAX_TEXT_INPUT) { break; } + if (i >= maxTextInput) { break; } // copy over character if we're allowed to input it if (keyboard_allow_character_input(*text)) { diff --git a/src/pc/controller/controller_keyboard.h b/src/pc/controller/controller_keyboard.h index 2451397b..6d5123ea 100644 --- a/src/pc/controller/controller_keyboard.h +++ b/src/pc/controller/controller_keyboard.h @@ -10,7 +10,7 @@ extern "C" { #endif -#define MAX_TEXT_INPUT 256 +#define MAX_TEXT_INPUT 255 extern char gTextInput[]; enum TextInputMode { @@ -23,7 +23,7 @@ bool keyboard_on_key_down(int scancode); bool keyboard_on_key_up(int scancode); void keyboard_on_all_keys_up(void); void keyboard_on_text_input(char* text); -char* keyboard_start_text_input(enum TextInputMode, void (*onEscape)(void), void (*onEnter)(void)); +char* keyboard_start_text_input(enum TextInputMode, u8 inMaxTextInput, void (*onEscape)(void), void (*onEnter)(void)); void keyboard_stop_text_input(void); bool keyboard_in_text_input(void); diff --git a/src/pc/network/discord/discord.c b/src/pc/network/discord/discord.c index a82c8a54..e23b7fb4 100644 --- a/src/pc/network/discord/discord.c +++ b/src/pc/network/discord/discord.c @@ -49,7 +49,11 @@ static void register_launch_command(void) { } #endif strncat(cmd, " --discord 1", MAX_LAUNCH_CMD - 1); - DISCORD_REQUIRE(app.activities->register_command(app.activities, cmd)); + int rc = app.activities->register_command(app.activities, cmd); + if (rc != DiscordResult_Ok) { + LOG_ERROR("register command failed %d", rc); + return; + } LOG_INFO("cmd: %s", cmd); } diff --git a/src/pc/network/network.c b/src/pc/network/network.c index 6f5fbad8..224bd60d 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -126,6 +126,7 @@ void network_receive(u8* data, u16 dataLength) { case PACKET_RESERVATION: network_receive_reservation(&p); break; case PACKET_SAVE_FILE_REQUEST: network_receive_save_file_request(&p); break; case PACKET_SAVE_FILE: network_receive_save_file(&p); break; + case PACKET_CHAT: network_receive_chat(&p); break; case PACKET_CUSTOM: network_receive_custom(&p); break; default: LOG_ERROR("received unknown packet: %d", p.buffer[0]); } diff --git a/src/pc/network/network.h b/src/pc/network/network.h index 2fc84107..b94c6bf0 100644 --- a/src/pc/network/network.h +++ b/src/pc/network/network.h @@ -47,6 +47,7 @@ enum PacketType { PACKET_RESERVATION, PACKET_SAVE_FILE_REQUEST, PACKET_SAVE_FILE, + PACKET_CHAT, PACKET_CUSTOM = 255, }; @@ -184,4 +185,8 @@ u8 network_register_custom_packet(void (*send_callback)(struct Packet* p, void* void network_send_custom(u8 customId, bool reliable, void* params); void network_receive_custom(struct Packet* p); +// packet_chat.c +void network_send_chat(char* message); +void network_receive_chat(struct Packet* p); + #endif diff --git a/src/pc/network/packets/packet_chat.c b/src/pc/network/packets/packet_chat.c new file mode 100644 index 00000000..cbbb9932 --- /dev/null +++ b/src/pc/network/packets/packet_chat.c @@ -0,0 +1,51 @@ +#include +#include "../network.h" +#include "game/chat.h" + +static u8 localChatId = 1; + +// two-player hack: the remoteChatIds stuff is only valid for the one remote player +// will need to be extended if MAX_PLAYERS is ever increased +#define MAX_CHAT_IDS 16 +static u8 remoteChatIds[MAX_CHAT_IDS] = { 0 }; +static u8 onRemoteChatId = 0; + +void network_send_chat(char* message) { + u16 messageLength = strlen(message); + struct Packet p; + packet_init(&p, PACKET_CHAT, true); + packet_write(&p, &localChatId, sizeof(u8)); + packet_write(&p, &messageLength, sizeof(u16)); + packet_write(&p, message, messageLength * sizeof(u8)); + + network_send(&p); + localChatId++; +} + +void network_receive_chat(struct Packet* p) { + u8 remoteChatId = 0; + u16 remoteMessageLength = 0; + char remoteMessage[255] = { 0 }; + + packet_read(p, &remoteChatId, sizeof(u8)); + packet_read(p, &remoteMessageLength, sizeof(u16)); + if (remoteMessageLength > 255) { remoteMessageLength = 254; } + packet_read(p, &remoteMessage, remoteMessageLength * sizeof(u8)); + + // check if remote chat id has already been seen + for (u16 i = 0; i < MAX_CHAT_IDS; i++) { + if (remoteChatIds[i] == remoteChatId) { + // we already saw this message! + return; + } + } + + // cache the seen id + remoteChatIds[onRemoteChatId] = remoteChatId; + onRemoteChatId = (onRemoteChatId + 1) % MAX_CHAT_IDS; + + // add the message + chat_add_message(remoteMessage, FALSE); + + return; +}