From 68ad6bc15c5427c5c86a987aea8cb38c774cee79 Mon Sep 17 00:00:00 2001 From: MysterD Date: Thu, 3 Sep 2020 18:30:15 -0700 Subject: [PATCH] Added host/join in-game GUI Now people aren't forced to launch with command-line parameters, instead they can host or join a server by selecting buttons in the main menu. --- levels/menu/script.c | 4 - src/game/ingame_menu.c | 30 +++++ src/game/ingame_menu.h | 3 +- src/menu/file_select.c | 253 +++++++++++++++++++++++++++++++++++++-- src/menu/file_select.h | 9 +- src/pc/network/network.c | 3 + src/pc/network/network.h | 1 + src/pc/pc_main.c | 2 +- 8 files changed, 291 insertions(+), 14 deletions(-) diff --git a/levels/menu/script.c b/levels/menu/script.c index bb7d5e2f..4e955c12 100644 --- a/levels/menu/script.c +++ b/levels/menu/script.c @@ -44,17 +44,13 @@ const LevelScript level_main_menu_entry_1[] = { FREE_LEVEL_POOL(), LOAD_AREA(/*area*/ 1), SET_MENU_MUSIC(/*seq*/ 0x0021), -#ifndef IMMEDIATELOAD TRANSITION(/*transType*/ WARP_TRANSITION_FADE_FROM_COLOR, /*time*/ 16, /*color*/ 0xFF, 0xFF, 0xFF), -#endif CALL(/*arg*/ 0, /*func*/ lvl_init_menu_values_and_cursor_pos), CALL_LOOP(/*arg*/ 0, /*func*/ lvl_update_obj_and_load_file_selected), GET_OR_SET(/*op*/ OP_SET, /*var*/ VAR_CURR_SAVE_FILE_NUM), STOP_MUSIC(/*fadeOutTime*/ 0x00BE), -#ifndef IMMEDIATELOAD TRANSITION(/*transType*/ WARP_TRANSITION_FADE_INTO_COLOR, /*time*/ 16, /*color*/ 0xFF, 0xFF, 0xFF), SLEEP(/*frames*/ 16), -#endif CLEAR_LEVEL(), SLEEP_BEFORE_EXIT(/*frames*/ 1), SET_REG(/*value*/ LEVEL_CASTLE_GROUNDS), diff --git a/src/game/ingame_menu.c b/src/game/ingame_menu.c index 2019091d..a4d89509 100644 --- a/src/game/ingame_menu.c +++ b/src/game/ingame_menu.c @@ -411,6 +411,36 @@ void render_multi_text_string(s16 *xPos, s16 *yPos, s8 multiTextID) } #endif +void str_ascii_to_dialog(char* string, char* dialog, int 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]); + } + } + dialog[length] = DIALOG_CHAR_TERMINATOR; +} + +void print_generic_ascii_string(s16 x, s16 y, const u8* ascii) { + u8 dialog[256] = { DIALOG_CHAR_TERMINATOR }; + str_ascii_to_dialog(ascii, dialog, strlen(ascii)); + print_generic_string(x, y, dialog); +} + #if defined(VERSION_JP) || defined(VERSION_SH) #define MAX_STRING_WIDTH 18 #else diff --git a/src/game/ingame_menu.h b/src/game/ingame_menu.h index 1435e164..ccab3a1e 100644 --- a/src/game/ingame_menu.h +++ b/src/game/ingame_menu.h @@ -8,7 +8,6 @@ ((asc) >= 'A' && (asc) <= 'Z') ? ((asc) - 'A' + 0x0A) : \ ((asc) >= 'a' && (asc) <= 'z') ? ((asc) - 'a' + 0x24) : 0x00) - #define MENU_MTX_PUSH 1 #define MENU_MTX_NOPUSH 2 @@ -117,6 +116,8 @@ extern s8 gRedCoinsCollected; 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(char* string, char* dialog, int length); +void print_generic_ascii_string(s16 x, s16 y, const u8* 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); void print_menu_generic_string(s16 x, s16 y, const u8 *str); diff --git a/src/menu/file_select.c b/src/menu/file_select.c index 5d063b91..8a27113d 100644 --- a/src/menu/file_select.c +++ b/src/menu/file_select.c @@ -21,6 +21,9 @@ #include "sm64.h" #include "text_strings.h" +#include "pc/controller/controller_keyboard.h" +#include "pc/network/network.h" + #include "eu_translation.h" #ifdef VERSION_EU #undef LANGUAGE_FUNCTION @@ -52,7 +55,7 @@ static s16 sSoundTextY; #define NUM_BUTTONS 34 #endif #else -#define NUM_BUTTONS 32 +#define NUM_BUTTONS 36 #endif // Amount of main menu buttons defined in the code called by spawn_object_rel_with_rot. @@ -372,6 +375,12 @@ static void bhv_menu_button_growing_from_main_menu(struct Object *button) { * Shrink back to main menu, used to return back while inside menus. */ static void bhv_menu_button_shrinking_to_main_menu(struct Object *button) { + // hack, make sure network button goes off-screen + if (button == sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]) { + button->oMenuButtonOrigPosX = 0; + button->oMenuButtonOrigPosY = -9999; + } + if (button->oMenuButtonTimer < 16) { button->oFaceAngleYaw -= 0x800; } @@ -1022,14 +1031,17 @@ void render_sound_mode_menu_buttons(struct Object *soundModeButton) { sMainMenuButtons[MENU_BUTTON_STEREO] = spawn_object_rel_with_rot( soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 533, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_STEREO]->oMenuButtonScale = 0.11111111f; + sMainMenuButtons[MENU_BUTTON_STEREO]->oFaceAngleRoll = 0; // Mono option button sMainMenuButtons[MENU_BUTTON_MONO] = spawn_object_rel_with_rot( soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, 0, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_MONO]->oMenuButtonScale = 0.11111111f; + sMainMenuButtons[MENU_BUTTON_MONO]->oFaceAngleRoll = 0; // Headset option button sMainMenuButtons[MENU_BUTTON_HEADSET] = spawn_object_rel_with_rot( soundModeButton, MODEL_MAIN_MENU_GENERIC_BUTTON, bhvMenuButton, -533, SOUND_BUTTON_Y, -100, 0, -0x8000, 0); sMainMenuButtons[MENU_BUTTON_HEADSET]->oMenuButtonScale = 0.11111111f; + sMainMenuButtons[MENU_BUTTON_HEADSET]->oFaceAngleRoll = 0; #ifdef VERSION_EU // English option button @@ -1115,13 +1127,10 @@ void check_sound_mode_menu_clicked_buttons(struct Object *soundModeButton) { * retuning sSelectedFileNum to a save value defined in fileNum. */ void load_main_menu_save_file(struct Object *fileButton, s32 fileNum) { -#ifdef IMMEDIATELOAD - sSelectedFileNum = fileNum; -#else if (fileButton->oMenuButtonState == MENU_BUTTON_STATE_FULLSCREEN) { sSelectedFileNum = fileNum; + network_init(NT_SERVER, "", NETWORK_DEFAULT_PORT); } -#endif } /** @@ -1162,6 +1171,11 @@ void return_to_main_menu(s16 prevMenuButtonID, struct Object *sourceButton) { mark_obj_for_deletion(sMainMenuButtons[buttonID]); } } + if (prevMenuButtonID == MENU_BUTTON_NETWORK_MODE) { + for (buttonID = MENU_BUTTON_NETWORK_MIN; buttonID < MENU_BUTTON_NETWORK_MAX; buttonID++) { + mark_obj_for_deletion(sMainMenuButtons[buttonID]); + } + } } } @@ -1353,6 +1367,10 @@ void bhv_menu_button_manager_init(void) { sMainMenuButtons[MENU_BUTTON_SOUND_MODE] = spawn_object_rel_with_rot( gCurrentObject, MODEL_MAIN_MENU_PURPLE_SOUND_BUTTON, bhvMenuButton, 6400, -3500, 0, 0, 0, 0); sMainMenuButtons[MENU_BUTTON_SOUND_MODE]->oMenuButtonScale = 1.0f; + // Network menu button + sMainMenuButtons[MENU_BUTTON_NETWORK_MODE] = spawn_object_rel_with_rot( + gCurrentObject, MODEL_MAIN_MENU_GREEN_SCORE_BUTTON, bhvMenuButton, 6400, -5500, 0, 0, 0, 0); + sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]->oMenuButtonScale = 1.0f; sTextBaseAlpha = 0; } @@ -1368,6 +1386,30 @@ void bhv_menu_button_manager_init(void) { * Also play a sound and/or render buttons depending of the button ID selected. */ void check_main_menu_clicked_buttons(void) { + + // force the network screen to open automatically + static u8 networkInit = FALSE; + if (!networkInit) { + sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]->oMenuButtonState = MENU_BUTTON_STATE_GROWING; + + struct Object* button = sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]; + button->oFaceAnglePitch = 0; + button->oFaceAngleYaw = 32768; + button->oFaceAngleRoll = 0; + button->oParentRelativePosX = 0.0f; + button->oParentRelativePosY = 0.0f; + button->oParentRelativePosZ = 17800.0f; + button->oMenuButtonOrigPosX = 0; + button->oMenuButtonOrigPosY = 0; + button->oMenuButtonOrigPosZ = -17800.0f; + button->oMenuButtonScale = 1.0f; + button->oMenuButtonState = MENU_BUTTON_STATE_FULLSCREEN; + button->oMenuButtonTimer = 0; + + sSelectedButtonID = MENU_BUTTON_NETWORK_MODE; + networkInit = TRUE; + } + #ifdef VERSION_EU if (sMainMenuTimer >= 5) { #endif @@ -1433,6 +1475,10 @@ void check_main_menu_clicked_buttons(void) { play_sound(SOUND_MENU_CAMERA_ZOOM_IN, gDefaultSoundArgs); render_sound_mode_menu_buttons(sMainMenuButtons[MENU_BUTTON_SOUND_MODE]); break; + case MENU_BUTTON_NETWORK_MODE: + play_sound(SOUND_MENU_CAMERA_ZOOM_IN, gDefaultSoundArgs); + render_network_mode_menu_buttons(sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]); + break; } #ifdef VERSION_EU } @@ -1541,6 +1587,17 @@ void bhv_menu_button_manager_loop(void) { check_sound_mode_menu_clicked_buttons(sMainMenuButtons[MENU_BUTTON_SOUND_MODE]); break; + case MENU_BUTTON_NETWORK_MODE: + check_network_mode_menu_clicked_buttons(sMainMenuButtons[MENU_BUTTON_NETWORK_MODE]); + break; + + case MENU_BUTTON_HOST: + return_to_main_menu(MENU_BUTTON_NETWORK_MODE, sMainMenuButtons[MENU_BUTTON_HOST]); + break; + case MENU_BUTTON_JOIN: + exit_join_to_network_menu(); + break; + // STEREO, MONO and HEADSET buttons are undefined so they can be selected without // exiting the Options menu, as a result they added a return button #ifdef VERSION_EU @@ -1597,11 +1654,16 @@ void handle_cursor_button_input(void) { sClickPos[1] = sCursorPos[1]; sCursorClickingTimer = 1; } -#ifdef IMMEDIATELOAD + if (networkType == NT_SERVER) { + sClickPos[0] = sCursorPos[0]; + sClickPos[1] = sCursorPos[1]; + sCursorClickingTimer = 1; + } +/*#ifdef IMMEDIATELOAD sClickPos[0] = sCursorPos[0]; sClickPos[1] = sCursorPos[1]; sCursorClickingTimer = 1; -#endif +#endif*/ } } @@ -2758,6 +2820,12 @@ static void print_file_select_strings(void) { case MENU_BUTTON_SOUND_MODE: print_sound_mode_menu_strings(); break; + case MENU_BUTTON_NETWORK_MODE: + print_network_mode_menu_strings(); + break; + case MENU_BUTTON_JOIN: + print_join_mode_menu_strings(); + break; } // If all 4 save file exists, define true to sAllFilesExist to prevent more copies in copy menu if (save_file_exists(SAVE_FILE_A) == TRUE && save_file_exists(SAVE_FILE_B) == TRUE && @@ -2842,6 +2910,16 @@ s32 lvl_init_menu_values_and_cursor_pos(UNUSED s32 arg, UNUSED s32 unused) { } } #endif + + // center cursor + sCursorPos[0] = 0.0f; + sCursorPos[1] = -24.0f; + + // immediately jump in + if (networkType != NT_NONE) { + sSelectedFileNum = 1; + } + //! no return value #ifdef AVOID_UB return 0; @@ -2857,3 +2935,164 @@ s32 lvl_update_obj_and_load_file_selected(UNUSED s32 arg, UNUSED s32 unused) { area_update_objects(); return sSelectedFileNum; } + +// --- custom network menu code --- // + +void exit_join_to_network_menu(void) { + // Begin exit + if (sMainMenuButtons[MENU_BUTTON_JOIN]->oMenuButtonState == MENU_BUTTON_STATE_FULLSCREEN + && sCursorClickingTimer == 2) { + play_sound(SOUND_MENU_CAMERA_ZOOM_OUT, gDefaultSoundArgs); + sMainMenuButtons[MENU_BUTTON_JOIN]->oMenuButtonState = MENU_BUTTON_STATE_SHRINKING; + keyboard_stop_text_input(); + } + // End exit + if (sMainMenuButtons[MENU_BUTTON_JOIN]->oMenuButtonState == MENU_BUTTON_STATE_DEFAULT) { + sSelectedButtonID = MENU_BUTTON_NETWORK_MODE; + if (sCurrentMenuLevel == MENU_LAYER_SUBMENU) { + sCurrentMenuLevel = MENU_LAYER_MAIN; + } + } +} + +void keyboard_exit_join_to_network_menu(void) { + sCursorClickingTimer = 2; + exit_join_to_network_menu(); +} + +void join_server_as_client(void) { + sCursorClickingTimer = 2; + char delims[] = { ' ' }; + + // trim whitespace + char* text = textInput; + while (*text == ' ') { text++; } + + // grab IP + char* ip = strtok(text, delims); + if (ip == NULL) { + exit_join_to_network_menu(); + return; + } + + // grab port + char* port = strtok(NULL, delims); + if (port != NULL && atoi(port) == 0) { + exit_join_to_network_menu(); + return; + } + + network_init(NT_CLIENT, textInput, port); + sSelectedFileNum = 1; +} + +void render_network_mode_menu_buttons(struct Object* soundModeButton) { + #define NETWORK_BUTTON_Y 0 + // Host option button + sMainMenuButtons[MENU_BUTTON_HOST] = spawn_object_rel_with_rot( + soundModeButton, MODEL_MAIN_MENU_MARIO_NEW_BUTTON, bhvMenuButton, 266, NETWORK_BUTTON_Y, -100, 0, -0x8000, 0); + sMainMenuButtons[MENU_BUTTON_HOST]->oMenuButtonScale = 0.11111111f; + sMainMenuButtons[MENU_BUTTON_HOST]->oFaceAngleRoll = 0; + + // Join option button + sMainMenuButtons[MENU_BUTTON_JOIN] = spawn_object_rel_with_rot( + soundModeButton, MODEL_MAIN_MENU_MARIO_NEW_BUTTON, bhvMenuButton, -266, NETWORK_BUTTON_Y, -100, 0, -0x8000, 0); + sMainMenuButtons[MENU_BUTTON_JOIN]->oMenuButtonScale = 0.11111111f; + sMainMenuButtons[MENU_BUTTON_JOIN]->oFaceAngleRoll = 0; +} + +void check_network_mode_menu_clicked_buttons(struct Object* networkModeButton) { + if (networkModeButton->oMenuButtonState == MENU_BUTTON_STATE_FULLSCREEN) { + s32 buttonID; + // Configure sound mode menu button group + for (buttonID = MENU_BUTTON_NETWORK_MIN; buttonID < MENU_BUTTON_NETWORK_MAX; buttonID++) { + s16 buttonX = sMainMenuButtons[buttonID]->oPosX; + s16 buttonY = sMainMenuButtons[buttonID]->oPosY; + + if (check_clicked_button(buttonX, buttonY, 22.0f) == TRUE) { + if (buttonID == MENU_BUTTON_HOST) { + if (networkModeButton->oMenuButtonActionPhase == SOUND_MODE_PHASE_MAIN) { + play_sound(SOUND_MENU_CLICK_FILE_SELECT, gDefaultSoundArgs); + sMainMenuButtons[buttonID]->oMenuButtonState = MENU_BUTTON_STATE_ZOOM_IN_OUT; + sSelectedButtonID = buttonID; + //sSoundMode = buttonID - MENU_BUTTON_OPTION_MIN; + } + } + else if (buttonID == MENU_BUTTON_JOIN) { + play_sound(SOUND_MENU_CAMERA_ZOOM_IN, gDefaultSoundArgs); + sMainMenuButtons[buttonID]->oMenuButtonState = MENU_BUTTON_STATE_GROWING; + sSelectedButtonID = buttonID; + keyboard_start_text_input(TIM_IP, keyboard_exit_join_to_network_menu, join_server_as_client); + } + sCurrentMenuLevel = MENU_LAYER_SUBMENU; + + break; + } + } + } +} + +void print_network_mode_menu_strings(void) { + s32 mode; + s16 textX; + #define HEADER_HUD_X 106 + unsigned char textHeader[10]; + str_ascii_to_dialog("SM64 COOP", textHeader, 9); + + // Print header text + gSPDisplayList(gDisplayListHead++, dl_rgba16_text_begin); + gDPSetEnvColor(gDisplayListHead++, 255, 255, 255, sTextBaseAlpha); + + print_hud_lut_string(HUD_LUT_DIFF, HEADER_HUD_X, 35, textHeader); + + gSPDisplayList(gDisplayListHead++, dl_rgba16_text_end); + + gSPDisplayList(gDisplayListHead++, dl_ia_text_begin); + + #define TEXT_HOST 0x11,0x18,0x1C,0x1D,0xFF + #define TEXT_JOIN 0x13,0x18,0x12,0x17,0xFF + static unsigned char textNetworkModes[][5] = { { TEXT_HOST }, { TEXT_JOIN } }; + + // Print network mode names + for (mode = 0; mode < 2; mode++) { + gDPSetEnvColor(gDisplayListHead++, 255, 255, 255, sTextBaseAlpha); + + textX = get_str_x_pos_from_center(mode * 72 + 124, textNetworkModes[mode], 10.0f); + print_generic_string(textX, 87, textNetworkModes[mode]); + } + + gSPDisplayList(gDisplayListHead++, dl_ia_text_end); +} + +void print_join_mode_menu_strings(void) { + #define JOIN_MARIO_X 25 + #define JOIN_FILE_LETTER_X 95 + #define JOIN_LEVEL_NAME_X 25 + #define JOIN_SECRET_STARS_X 171 + #define JOIN_MYSCORE_X 238 + #define JOIN_HISCORE_X 231 + + unsigned char textMario[8]; + str_ascii_to_dialog("CONNECT", textMario, 7); + + void** levelNameTable = segmented_to_virtual(seg2_course_name_table); + + // Print file name at top + gSPDisplayList(gDisplayListHead++, dl_rgba16_text_begin); + gDPSetEnvColor(gDisplayListHead++, 255, 255, 255, sTextBaseAlpha); + print_hud_lut_string(HUD_LUT_DIFF, JOIN_MARIO_X, 15, textMario); + + // Print course scores + gSPDisplayList(gDisplayListHead++, dl_menu_ia8_text_begin); + gDPSetEnvColor(gDisplayListHead++, 255, 255, 255, sTextBaseAlpha); + + // Print level name + print_generic_ascii_string(JOIN_LEVEL_NAME_X, 191 - (12 * 0), "Type or paste the host's IP."); + print_generic_ascii_string(JOIN_LEVEL_NAME_X, 191 - (12 * 2), textInput); + + if (strlen(textInput) > 0) { + print_generic_ascii_string(JOIN_LEVEL_NAME_X, 191 - (12 * 14), "Press (ENTER) to join."); + } + + gSPDisplayList(gDisplayListHead++, dl_menu_ia8_text_end); +} diff --git a/src/menu/file_select.h b/src/menu/file_select.h index 359677eb..91fbb81d 100644 --- a/src/menu/file_select.h +++ b/src/menu/file_select.h @@ -87,7 +87,14 @@ enum MenuButtonTypes { MENU_BUTTON_LANGUAGE_RETURN, #endif - MENU_BUTTON_OPTION_MAX + MENU_BUTTON_OPTION_MAX, + + MENU_BUTTON_NETWORK_MODE, + MENU_BUTTON_NETWORK_MIN, + MENU_BUTTON_HOST = MENU_BUTTON_NETWORK_MIN, + MENU_BUTTON_JOIN, + MENU_BUTTON_NETWORK_MAX, + }; enum ScoreMenuMessageID { diff --git a/src/pc/network/network.c b/src/pc/network/network.c index 57b2bcd9..fc946723 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -16,6 +16,9 @@ void network_init(enum NetworkType inNetworkType, char* ip, char* port) { networkType = inNetworkType; if (networkType == NT_NONE) { return; } + if (port == NULL) { + port = NETWORK_DEFAULT_PORT; + } // Create a receiver socket to receive datagrams gSocket = socket_initialize(); diff --git a/src/pc/network/network.h b/src/pc/network/network.h index 4512053f..b1a00a2d 100644 --- a/src/pc/network/network.h +++ b/src/pc/network/network.h @@ -13,6 +13,7 @@ #define MAX_SYNC_OBJECT_FIELDS 64 #define PACKET_LENGTH 1024 #define NETWORKTYPESTR (networkType == NT_CLIENT ? "Client" : "Server") +#define NETWORK_DEFAULT_PORT "7777" enum PacketType { PACKET_ACK, diff --git a/src/pc/pc_main.c b/src/pc/pc_main.c index bffbda31..5ca2a388 100644 --- a/src/pc/pc_main.c +++ b/src/pc/pc_main.c @@ -243,7 +243,7 @@ void main_func(void) { #endif char window_title[96] = - "Super Mario 64 coop EX (" RAPI_NAME ")" + "Super Mario 64 EX coop (" RAPI_NAME ")" #ifdef NIGHTLY " nightly " GIT_HASH #endif