diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index cdfbec94e..f5f07facf 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -282,7 +282,7 @@ jobs: bundle: io.github.fgsfdsfgs.perfect_dark.flatpak manifest-path: dist/linux/io.github.fgsfdsfgs.perfect_dark.yaml cache-key: flatpak-builder-${{ github.sha }} - upload-artifact: true + upload-artifact: false # - name: Build Flatpak bundle # run: | diff --git a/README.md b/README.md index 00485eebb..53022f2bd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ There are minor graphics- and gameplay-related issues, and possibly occasional c * basic mod support, currently enough to load a few custom levels; * slightly expanded memory heap size; * experimental high framerate support (up to 240 FPS): - * set `Game.TickRateDivisor` to `0` in `pd.ini` to activate; + * enable `Uncap Tickrate` in `Extended Video Options` to activate; * in practice the game will have issues running faster than ~165 FPS, so use VSync or `Video.FramerateLimit` to cap it. * emulate the Transfer Pak functionality the game has on the Nintendo 64 to unlock some cheats automatically. diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index 043a7e2f2..e974d4fd4 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -30,10 +30,30 @@ static uint64_t qpc_freq; #define FRAME_INTERVAL_US_NUMERATOR 1000000 #define FRAME_INTERVAL_US_DENOMINATOR (target_fps) -static void set_fullscreen(bool on, bool call_callback) { - if (fullscreen_state == on) { - return; +static int32_t gfx_sdl_get_maximized_state(void) { + return (int32_t)maximized_state; +} + +static int32_t gfx_sdl_get_fullscreen_state(void) { + return (int32_t)fullscreen_state; +} + +static int32_t gfx_sdl_get_fullscreen_flag_mode(void) { + return fullscreen_flag == SDL_WINDOW_FULLSCREEN_DESKTOP ? 0 : 1; +} + +static void gfx_sdl_set_fullscreen_flag(int32_t mode) { + switch (mode) { + case 0: { + fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP; + } break; + case 1: { + fullscreen_flag = SDL_WINDOW_FULLSCREEN; + } break; } +} + +static void set_fullscreen(bool on, bool call_callback) { fullscreen_state = on; SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0); if (call_callback && on_fullscreen_changed_callback) { @@ -42,9 +62,6 @@ static void set_fullscreen(bool on, bool call_callback) { } static void set_maximize_window(bool on) { - if (maximized_state == on) { - return; - } maximized_state = on; if (on) { SDL_MaximizeWindow(wnd); @@ -95,6 +112,13 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { posY = 100; } + if (set->centered) { + SDL_DisplayMode mode = {}; + SDL_GetCurrentDisplayMode(0, &mode); + posX = mode.w / 2 - window_width / 2; + posY = mode.h / 2 - window_height / 2; + } + if (set->fullscreen_is_exclusive) { fullscreen_flag = SDL_WINDOW_FULLSCREEN; } @@ -222,6 +246,43 @@ static void gfx_sdl_set_cursor_visibility(bool visible) { } } +static void get_centered_positions_native(int32_t width, int32_t height, int32_t *posX, int32_t *posY) { + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode mode = {}; + SDL_GetDesktopDisplayMode(disp_idx, &mode); + *posX = mode.w / 2 - width / 2; + *posY = mode.h / 2 - height / 2; +} + +static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) { + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode mode = {}; + SDL_GetCurrentDisplayMode(disp_idx, &mode); + *posX = mode.w / 2 - width / 2; + *posY = mode.h / 2 - height / 2; +} + +static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) { + const SDL_DisplayMode mode = {.w = width, .h = height}; + const int disp_idx = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode closest = {}; + if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) { + SDL_SetWindowDisplayMode(wnd, &closest); + SDL_SetWindowSize(wnd, closest.w, closest.h); + if (should_center) { + int32_t posX = 0; + int32_t posY = 0; + get_centered_positions_native(closest.w, closest.h, &posX, &posY); + SDL_SetWindowPosition(wnd, posX, posY); + } + } +} + +static void gfx_sdl_set_dimensions(uint32_t width, uint32_t height, int32_t posX, int32_t posY) { + SDL_SetWindowSize(wnd, width, height); + SDL_SetWindowPosition(wnd, posX, posY); +} + static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY) { SDL_GL_GetDrawableSize(wnd, static_cast((void*)width), static_cast((void*)height)); SDL_GetWindowPosition(wnd, static_cast(posX), static_cast(posY)); @@ -240,6 +301,9 @@ static void gfx_sdl_handle_events(void) { case SDL_WINDOWEVENT: if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { SDL_GL_GetDrawableSize(wnd, &window_width, &window_height); + if (!fullscreen_state) { + maximized_state = SDL_GetWindowFlags(wnd) & SDL_WINDOW_MAXIMIZED ? true : false; + } } else if (event.window.event == SDL_WINDOWEVENT_CLOSE && event.window.windowID == SDL_GetWindowID(wnd)) { // We listen specifically for main window close because closing main window @@ -304,6 +368,10 @@ static double gfx_sdl_get_time(void) { return SDL_GetPerformanceCounter() / (double)qpc_freq; } +static int32_t gfx_sdl_get_target_fps(void) { + return target_fps; +} + static void gfx_sdl_set_target_fps(int fps) { target_fps = fps; } @@ -320,6 +388,10 @@ static void gfx_sdl_set_window_title(const char *title) { SDL_SetWindowTitle(wnd, title); } +static int gfx_sdl_get_swap_interval(void) { + return SDL_GL_GetSwapInterval(); +} + static bool gfx_sdl_set_swap_interval(int interval) { const bool success = SDL_GL_SetSwapInterval(interval) >= 0; vsync_enabled = success && (interval != 0); @@ -329,24 +401,63 @@ static bool gfx_sdl_set_swap_interval(int interval) { return success; } -struct GfxWindowManagerAPI gfx_sdl = { +int gfx_sdl_get_display_mode(int modenum, int *out_w, int *out_h) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode sdlmode; + if (SDL_GetDisplayMode(display_in_use, modenum, &sdlmode) == 0) { + *out_w = sdlmode.w; + *out_h = sdlmode.h; + return 1; + } + return 0; +} + +int gfx_sdl_get_current_display_mode(int *out_w, int *out_h) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + SDL_DisplayMode sdlmode; + if (SDL_GetCurrentDisplayMode(display_in_use, &sdlmode) == 0) { + *out_w = sdlmode.w; + *out_h = sdlmode.h; + return 1; + } + return 0; +} + +int gfx_sdl_get_num_display_modes(void) { + const int display_in_use = SDL_GetWindowDisplayIndex(wnd); + return SDL_GetNumDisplayModes(display_in_use); +} + +struct GfxWindowManagerAPI gfx_sdl = { gfx_sdl_init, gfx_sdl_close, + gfx_sdl_get_display_mode, + gfx_sdl_get_current_display_mode, + gfx_sdl_get_num_display_modes, + gfx_sdl_get_fullscreen_state, gfx_sdl_set_fullscreen_changed_callback, gfx_sdl_set_fullscreen, gfx_sdl_set_fullscreen_exclusive, + gfx_sdl_set_fullscreen_flag, + gfx_sdl_get_fullscreen_flag_mode, + gfx_sdl_get_maximized_state, gfx_sdl_set_maximize_window, gfx_sdl_get_active_window_refresh_rate, gfx_sdl_set_cursor_visibility, + gfx_sdl_set_closest_resolution, + gfx_sdl_set_dimensions, gfx_sdl_get_dimensions, + gfx_sdl_get_centered_positions, gfx_sdl_handle_events, gfx_sdl_start_frame, gfx_sdl_swap_buffers_begin, gfx_sdl_swap_buffers_end, gfx_sdl_get_time, + gfx_sdl_get_target_fps, gfx_sdl_set_target_fps, gfx_sdl_can_disable_vsync, gfx_sdl_get_window_handle, gfx_sdl_set_window_title, + gfx_sdl_get_swap_interval, gfx_sdl_set_swap_interval, }; diff --git a/port/fast3d/gfx_window_manager_api.h b/port/fast3d/gfx_window_manager_api.h index 61b05315f..d10fc45cf 100644 --- a/port/fast3d/gfx_window_manager_api.h +++ b/port/fast3d/gfx_window_manager_api.h @@ -13,28 +13,41 @@ struct GfxWindowInitSettings { bool fullscreen; bool fullscreen_is_exclusive; bool maximized; + bool centered; bool allow_hidpi; }; struct GfxWindowManagerAPI { void (*init)(const struct GfxWindowInitSettings *settings); void (*close)(void); + int (*get_display_mode)(int modenum, int *out_w, int *out_h); + int (*get_current_display_mode)(int *out_w, int *out_h); + int (*get_num_display_modes)(void); + int32_t (*get_fullscreen_state)(void); void (*set_fullscreen_changed_callback)(void (*on_fullscreen_changed)(bool is_now_fullscreen)); void (*set_fullscreen)(bool enable); void (*set_fullscreen_exclusive)(bool exc); + void (*set_fullscreen_flag)(int32_t mode); + int32_t (*get_fullscreen_flag_mode)(void); + int32_t (*get_maximized_state)(void); void (*set_maximize)(bool enable); void (*get_active_window_refresh_rate)(uint32_t* refresh_rate); void (*set_cursor_visibility)(bool visible); + void (*set_closest_resolution)(int32_t width, int32_t height, bool should_center); + void (*set_dimensions)(uint32_t width, uint32_t height, int32_t posX, int32_t posY); void (*get_dimensions)(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY); + void (*get_centered_positions)(int32_t width, int32_t height, int32_t *posX, int32_t *posY); void (*handle_events)(void); bool (*start_frame)(void); void (*swap_buffers_begin)(void); void (*swap_buffers_end)(void); double (*get_time)(void); // For debug + int32_t (*get_target_fps)(void); void (*set_target_fps)(int fps); bool (*can_disable_vsync)(void); void *(*get_window_handle)(void); void (*set_window_title)(const char *); + int (*get_swap_interval)(void); bool (*set_swap_interval)(int); }; diff --git a/port/include/input.h b/port/include/input.h index 916cd72ba..6d5d88bfd 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -142,6 +142,10 @@ void inputControllerSetSticksSwapped(s32 cidx, s32 swapped); s32 inputControllerGetDualAnalog(s32 cidx); void inputControllerSetDualAnalog(s32 cidx, s32 enable); +// get/set Input.Player%d.CancelCButtons +s32 inputControllerGetCancelCButtons(s32 cidx); +void inputControllerSetCancelCButtons(s32 cidx, s32 cancel); + // get/set sensitivity for a given player f32 inputControllerGetAxisScale(s32 cidx, s32 stick, s32 axis); void inputControllerSetAxisScale(s32 cidx, s32 stick, s32 axis, f32 value); diff --git a/port/include/video.h b/port/include/video.h index d7a888886..288c9b284 100644 --- a/port/include/video.h +++ b/port/include/video.h @@ -11,6 +11,11 @@ #define VIDEO_MAX_FPS 240 #endif +typedef struct { + s32 width; + s32 height; +} displaymode; + s32 videoInit(void); void videoStartFrame(void); void videoSubmitCommands(Gfx *cmds); @@ -27,17 +32,36 @@ s32 videoGetWidth(void); s32 videoGetHeight(void); f32 videoGetAspect(void); s32 videoGetFullscreen(void); +s32 videoGetFullscreenMode(void); s32 videoGetMaximizeWindow(void); void videoSetMaximizeWindow(s32 fs); +s32 videoGetCenterWindow(void); +void videoSetCenterWindow(s32 center); u32 videoGetTextureFilter(void); s32 videoGetTextureFilter2D(void); s32 videoGetDetailTextures(void); +s32 videoGetDisplayModeIndex(void); +s32 videoGetDisplayMode(displaymode *out, const s32 index); +s32 videoGetNumDisplayModes(void); +s32 videoGetVsync(void); +s32 videoGetFramerateLimit(void); +s32 videoGetDisplayFPS(void); +s32 videoGetMSAA(void); + +f32 videoGetAverageFPS(void); +f64 videoGetLastRenderTime(void); void videoSetWindowOffset(s32 x, s32 y); void videoSetFullscreen(s32 fs); +void videoSetFullscreenMode(s32 mode); void videoSetTextureFilter(u32 filter); void videoSetTextureFilter2D(s32 filter); void videoSetDetailTextures(s32 detail); +void videoSetDisplayMode(const s32 index); +void videoSetVsync(const s32 vsync); +void videoSetFramerateLimit(const s32 limit); +void videoSetDisplayFPS(const s32 displayfps); +void videoSetMSAA(const s32 msaa); s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize); void videoSetFramebuffer(s32 target); @@ -49,4 +73,6 @@ s32 videoFramebuffersSupported(void); void videoResetTextureCache(void); void videoFreeCachedTexture(const void *texptr); +void videoShutdown(void); + #endif diff --git a/port/src/config.c b/port/src/config.c index 6be6f0d9f..596cf3a5a 100644 --- a/port/src/config.c +++ b/port/src/config.c @@ -10,7 +10,7 @@ #define CONFIG_MAX_SECNAME 128 #define CONFIG_MAX_KEYNAME 256 -#define CONFIG_MAX_SETTINGS 256 +#define CONFIG_MAX_SETTINGS 300 typedef enum { CFG_NONE, @@ -34,6 +34,7 @@ struct configentry { } settings[CONFIG_MAX_SETTINGS]; static s32 numSettings = 0; +static u8 configMaxWarningLogged = 0; static inline s32 configClampInt(s32 val, s32 min, s32 max) { @@ -64,11 +65,15 @@ static inline struct configentry *configAddEntry(const char *key) { if (numSettings < CONFIG_MAX_SETTINGS) { struct configentry *cfg = &settings[numSettings++]; - strncpy(cfg->key, key, CONFIG_MAX_KEYNAME); + snprintf(cfg->key, CONFIG_MAX_KEYNAME, "%s", key); const char *delim = strrchr(cfg->key, '.'); cfg->seclen = delim ? (delim - cfg->key) : 0; return cfg; } + if (!configMaxWarningLogged) { + sysLogPrintf(LOG_WARNING, "Maximum number of configuration entries exceeded: %d", CONFIG_MAX_SETTINGS); + configMaxWarningLogged = 1; + } return NULL; } diff --git a/port/src/input.c b/port/src/input.c index 90fd58084..e03989207 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -45,7 +45,8 @@ static SDL_GameController *pads[INPUT_MAX_CONTROLLERS]; .deadzone = { DEFAULT_DEADZONE, DEFAULT_DEADZONE, DEFAULT_DEADZONE, DEFAULT_DEADZONE_RY }, \ .stickCButtons = 0, \ .swapSticks = 1, \ - .deviceIndex = -1 \ + .deviceIndex = -1, \ + .cancelCButtons = 0, \ } static struct controllercfg { @@ -57,6 +58,7 @@ static struct controllercfg { s32 stickCButtons; s32 swapSticks; s32 deviceIndex; + s32 cancelCButtons; } padsCfg[INPUT_MAX_CONTROLLERS] = { CONTROLLERCFG_DEFAULT, CONTROLLERCFG_DEFAULT, @@ -771,12 +773,22 @@ s32 inputReadController(s32 idx, OSContPad *npad) npad->stick_x = xdiff < 0 ? -0x80 : (xdiff > 0 ? 0x7F : 0); npad->stick_y = ydiff < 0 ? -0x80 : (ydiff > 0 ? 0x7F : 0); + const struct controllercfg *cfg = &padsCfg[idx]; + + if (cfg->cancelCButtons) { + // opposite C buttons cancel each other out + if ((npad->button & (L_CBUTTONS | R_CBUTTONS)) == (L_CBUTTONS | R_CBUTTONS)) { + npad->button &= ~(L_CBUTTONS | R_CBUTTONS); + } + if ((npad->button & (U_CBUTTONS | D_CBUTTONS)) == (U_CBUTTONS | D_CBUTTONS)) { + npad->button &= ~(U_CBUTTONS | D_CBUTTONS); + } + } + if (!pads[idx]) { return 0; } - const struct controllercfg *cfg = &padsCfg[idx]; - s32 leftX = SDL_GameControllerGetAxis(pads[idx], cfg->axisMap[0][0]); s32 leftY = SDL_GameControllerGetAxis(pads[idx], cfg->axisMap[0][1]); s32 rightX = SDL_GameControllerGetAxis(pads[idx], cfg->axisMap[1][0]); @@ -965,6 +977,16 @@ void inputControllerSetDualAnalog(s32 cidx, s32 enable) padsCfg[cidx].stickCButtons = !enable; } +s32 inputControllerGetCancelCButtons(s32 cidx) +{ + return padsCfg[cidx].cancelCButtons; +} + +void inputControllerSetCancelCButtons(s32 cidx, s32 cancel) +{ + padsCfg[cidx].cancelCButtons = cancel; +} + f32 inputControllerGetAxisScale(s32 cidx, s32 stick, s32 axis) { return padsCfg[cidx].sens[stick * 2 + axis]; @@ -1490,6 +1512,7 @@ PD_CONSTRUCTOR static void inputConfigInit(void) configRegisterFloat(strFmt("%s.RStickScaleX", secname), &padsCfg[c].sens[2], -10.f, 10.f); configRegisterFloat(strFmt("%s.RStickScaleY", secname), &padsCfg[c].sens[3], -10.f, 10.f); configRegisterInt(strFmt("%s.StickCButtons", secname), &padsCfg[c].stickCButtons, 0, 1); + configRegisterInt(strFmt("%s.CancelCButtons", secname), &padsCfg[c].cancelCButtons, 0, 1); configRegisterInt(strFmt("%s.SwapSticks", secname), &padsCfg[c].swapSticks, 0, 1); configRegisterInt(strFmt("%s.ControllerIndex", secname), &padsCfg[c].deviceIndex, -1, 0x7FFFFFFF); secname[13] = '.'; diff --git a/port/src/main.c b/port/src/main.c index 424f380f5..3055ceec6 100644 --- a/port/src/main.c +++ b/port/src/main.c @@ -89,6 +89,7 @@ static void cleanup(void) sysLogPrintf(LOG_NOTE, "shutdown"); inputSaveBinds(); configSave(CONFIG_PATH); + videoShutdown(); crashShutdown(); // TODO: actually shut down all subsystems } @@ -184,5 +185,6 @@ PD_CONSTRUCTOR static void gameConfigInit(void) configRegisterUInt(strFmt("Game.Player%d.CrosshairColour", i), &g_PlayerExtCfg[j].crosshaircolour, 0, 0xFFFFFFFF); configRegisterUInt(strFmt("Game.Player%d.CrosshairSize", i), &g_PlayerExtCfg[j].crosshairsize, 0, 4); configRegisterInt(strFmt("Game.Player%d.CrosshairHealth", i), &g_PlayerExtCfg[j].crosshairhealth, 0, CROSSHAIR_HEALTH_ON_WHITE); + configRegisterInt(strFmt("Game.Player%d.UseKeyReloads", i), &g_PlayerExtCfg[j].usereloads, 0, false); } } diff --git a/port/src/optionsmenu.c b/port/src/optionsmenu.c index 91353705b..4771fc28b 100644 --- a/port/src/optionsmenu.c +++ b/port/src/optionsmenu.c @@ -698,19 +698,164 @@ static MenuItemHandlerResult menuhandlerFullScreen(s32 operation, struct menuite return 0; } -static MenuItemHandlerResult menuhandlerMaximizeWindow(s32 operation, struct menuitem *item, union handlerdata *data) +static MenuItemHandlerResult menuhandlerFullScreenMode(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const char *opts[] = { + "Borderless", + "Exclusive" + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = ARRAYCOUNT(opts); + break; + case MENUOP_GETOPTIONTEXT: + return (intptr_t)opts[data->dropdown.value]; + case MENUOP_SET: + videoSetFullscreenMode(data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetFullscreenMode(); + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerCenterWindow(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { case MENUOP_GET: - return videoGetMaximizeWindow(); + return videoGetCenterWindow(); case MENUOP_SET: - videoSetMaximizeWindow(data->checkbox.value); + videoSetCenterWindow(data->checkbox.value); break; } return 0; } +static MenuItemHandlerResult menuhandlerVsync(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const s32 numOpts = 10; + static const char *constOpts[] = { + "Adaptive", + "Off", + "On" + }; + static char dynOpt[20]; + s32 vblanks; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = numOpts; + break; + case MENUOP_GETOPTIONTEXT: + if (data->dropdown.value < ARRAYCOUNT(constOpts)) + return (intptr_t)constOpts[data->dropdown.value]; + vblanks = (s32)data->dropdown.value - 1; + snprintf(dynOpt, sizeof(dynOpt), "On (%d frames)", vblanks); + return (intptr_t)dynOpt; + case MENUOP_SET: + videoSetVsync(data->dropdown.value - 1); + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetVsync() + 1; + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerFramerateLimit(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GETSLIDER: + data->slider.value = videoGetFramerateLimit(); + break; + case MENUOP_SET: + videoSetFramerateLimit(data->slider.value); + break; + case MENUOP_GETSLIDERLABEL: + // NOTE: data->slider.label length must not exceed 15. + if (data->slider.value == 0) { + strcpy(data->slider.label, "Off"); + } else { + sprintf(data->slider.label, "%d FPS", data->slider.value); + } + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerMSAA(s32 operation, struct menuitem *item, union handlerdata *data) +{ + s32 msaa; + static const char *opts[] = { + "Off", + "2x (MSAA)", + "4x (MSAA)", + "8x (MSAA)", + "16x (MSAA)" + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = ARRAYCOUNT(opts); + break; + case MENUOP_GETOPTIONTEXT: + return (intptr_t)opts[data->dropdown.value]; + case MENUOP_SET: + videoSetMSAA(1 << data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + msaa = videoGetMSAA(); + if (msaa < 2) { + data->dropdown.value = 0; + } else if (msaa < 4) { + data->dropdown.value = 1; + } else if (msaa < 8) { + data->dropdown.value = 2; + } else if (msaa < 16) { + data->dropdown.value = 3; + } else { + data->dropdown.value = 4; + } + } + + return 0; +} + +static MenuItemHandlerResult menuhandlerResolution(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static char resstring[32]; + static const char *rescustom = "Custom"; + displaymode mode; + + switch (operation) { + case MENUOP_CHECKDISABLED: + if (videoGetFullscreen() && videoGetFullscreenMode() == 0) { + return true; + } + break; + case MENUOP_GETOPTIONCOUNT: + data->dropdown.value = videoGetNumDisplayModes(); + break; + case MENUOP_GETOPTIONTEXT: + videoGetDisplayMode(&mode, data->dropdown.value); + if (mode.width == 0 && mode.height == 0) { + return (intptr_t)rescustom; + } else { + snprintf(resstring, sizeof(resstring), "%dx%d", mode.width, mode.height); + } + return (intptr_t)resstring; + case MENUOP_SET: + videoSetDisplayMode(data->dropdown.value); + break; + case MENUOP_GETSELECTEDINDEX: + data->dropdown.value = videoGetDisplayModeIndex(); + } + + return 0; +} + static MenuItemHandlerResult menuhandlerTexFilter(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -755,12 +900,25 @@ static MenuItemHandlerResult menuhandlerTexFilter2D(s32 operation, struct menuit return videoGetTextureFilter2D(); case MENUOP_SET: videoSetTextureFilter2D(data->checkbox.value); + g_TexFilter2D = videoGetTextureFilter2D() ? G_TF_BILERP : G_TF_POINT; break; } return 0; } +static MenuItemHandlerResult menuhandlerDisplayFPS(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GET: + return videoGetDisplayFPS(); + case MENUOP_SET: + videoSetDisplayFPS(data->checkbox.value); + } + + return 0; +} + static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct menuitem *item, union handlerdata *data) { switch (operation) { @@ -774,6 +932,19 @@ static MenuItemHandlerResult menuhandlerGeMuzzleFlashes(s32 operation, struct me return 0; } +static MenuItemHandlerResult menuhandlerUncapTickrate(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_GET: + return (g_TickRateDiv == 0); + case MENUOP_SET: + g_TickRateDiv = !data->checkbox.value; + break; + } + + return 0; +} + static MenuItemHandlerResult menuhandlerCenterHUD(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -832,20 +1003,84 @@ struct menuitem g_ExtendedVideoMenuItems[] = { menuhandlerFullScreen, }, { - MENUITEMTYPE_CHECKBOX, + MENUITEMTYPE_DROPDOWN, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"Maximize Window", + (uintptr_t)"Full Screen Mode", 0, - menuhandlerMaximizeWindow, + menuhandlerFullScreenMode, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Resolution", + 0, + menuhandlerResolution, }, { MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"Detail Textures", + (uintptr_t)"Center Window", 0, - menuhandlerTexDetail, + menuhandlerCenterWindow, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Anti-aliasing", + 0, + menuhandlerMSAA, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, + }, + { + MENUITEMTYPE_DROPDOWN, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Vsync", + 0, + menuhandlerVsync, + }, + { + MENUITEMTYPE_SLIDER, + 0, + MENUITEMFLAG_LITERAL_TEXT | MENUITEMFLAG_SLIDER_WIDE | MENUITEMFLAG_SLIDER_DEFERRED, + (uintptr_t)"Framerate Limit", + VIDEO_MAX_FPS, + menuhandlerFramerateLimit, + }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Uncap Tickrate", + 0, + menuhandlerUncapTickrate, + }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Display FPS", + 0, + menuhandlerDisplayFPS, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, }, { MENUITEMTYPE_DROPDOWN, @@ -867,9 +1102,17 @@ struct menuitem g_ExtendedVideoMenuItems[] = { MENUITEMTYPE_CHECKBOX, 0, MENUITEMFLAG_LITERAL_TEXT, - (uintptr_t)"GE64-style Muzzle Flashes", + (uintptr_t)"Detail Textures", 0, - menuhandlerGeMuzzleFlashes, + menuhandlerTexDetail, + }, + { + MENUITEMTYPE_SEPARATOR, + 0, + 0, + 0, + 0, + NULL, }, { MENUITEMTYPE_DROPDOWN, @@ -879,6 +1122,14 @@ struct menuitem g_ExtendedVideoMenuItems[] = { 0, menuhandlerCenterHUD, }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"GE64-style Muzzle Flashes", + 0, + menuhandlerGeMuzzleFlashes, + }, { MENUITEMTYPE_SLIDER, 0, @@ -965,6 +1216,21 @@ struct menudialogdef g_ExtendedAudioMenuDialog = { NULL, }; +static MenuItemHandlerResult menuhandlerUseKeyReloads(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_CHECKDISABLED: + return !g_PlayerExtCfg[g_ExtMenuPlayer].extcontrols; + case MENUOP_GET: + return g_PlayerExtCfg[g_ExtMenuPlayer].usereloads; + case MENUOP_SET: + g_PlayerExtCfg[g_ExtMenuPlayer].usereloads = data->checkbox.value; + break; + } + + return 0; +} + static MenuItemHandlerResult menuhandlerCrouchMode(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *opts[] = { @@ -1269,6 +1535,14 @@ struct menuitem g_ExtendedGameMenuItems[] = { 0, menuhandlerCrosshairHealth, }, + { + MENUITEMTYPE_CHECKBOX, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Use Key Reloads", + 0, + menuhandlerUseKeyReloads, + }, { MENUITEMTYPE_SEPARATOR, 0, diff --git a/port/src/video.c b/port/src/video.c index 52eafc228..296a71063 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -6,6 +6,7 @@ #include #include "platform.h" #include "config.h" +#include "system.h" #include "video.h" #include "../fast3d/gfx_api.h" @@ -35,19 +36,33 @@ static s32 vidFramebuffers = true; static s32 vidFullscreen = DEFAULT_VID_FULLSCREEN; static s32 vidFullscreenExclusive = DEFAULT_VID_FULLSCREEN_EXCLUSIVE; static s32 vidMaximize = false; +static s32 vidCenter = false; static s32 vidAllowHiDpi = false; static s32 vidVsync = 1; static s32 vidMSAA = 1; static s32 vidFramerateLimit = 0; +static s32 vidDisplayFPS = 0; +static f32 vidDisplayFPSInterval = 1.f; +static f32 vidAvgFPS = 0; +static f64 vidLastRenderTime; + +static s32 vidNumModes = 1; +static displaymode vidModeDefault; +static displaymode *vidModes = &vidModeDefault; + static s32 texFilter = FILTER_LINEAR; static s32 texFilter2D = true; -static s32 texDetail = true; +static s32 texDetail = false; static u32 dlcount = 0; static u32 frames = 0; -static u32 framesPerSec = 0; -static f64 startTime, endTime, fpsTime; +static f64 startTime, endTime; +static f64 accumDelta = 0.0; +static f64 fpsTime = 0.0; +static s32 fpsNumFrames = 0; + +static s32 videoInitDisplayModes(void); s32 videoInit(void) { @@ -73,22 +88,17 @@ s32 videoInit(void) .fullscreen = vidFullscreen, .fullscreen_is_exclusive = vidFullscreenExclusive, .maximized = vidMaximize, + .centered = vidCenter, .allow_hidpi = vidAllowHiDpi } }; gfx_init(&set); - if (!wmAPI->set_swap_interval(vidVsync)) { - vidVsync = 0; - } + videoInitDisplayModes(); + videoSetVsync(vidVsync); + videoSetFramerateLimit(vidFramerateLimit); - if (vidVsync == 0 && vidFramerateLimit == 0) { - // cap FPS if there's no vsync to prevent the game from exploding - vidFramerateLimit = VIDEO_MAX_FPS; - } - - wmAPI->set_target_fps(vidFramerateLimit); // disabled because vsync is on gfx_set_texture_filter((enum FilteringMode)texFilter); initDone = true; @@ -101,6 +111,10 @@ void videoStartFrame(void) startTime = wmAPI->get_time(); gfx_start_frame(); } + + // Synchronize with their backend counterparts. + vidFullscreen = videoGetFullscreen(); + vidMaximize = videoGetMaximizeWindow(); } void videoSubmitCommands(Gfx *cmds) @@ -119,20 +133,35 @@ void videoEndFrame(void) gfx_end_frame(); - endTime = wmAPI->get_time(); - ++frames; - ++framesPerSec; + ++fpsNumFrames; + + const f64 flipTime = wmAPI->get_time(); + accumDelta += flipTime - endTime; + endTime = flipTime; + vidLastRenderTime = endTime - startTime; if (endTime >= fpsTime) { char tmp[128]; - snprintf(tmp, sizeof(tmp), "fps %3u frt %lf frm %u", framesPerSec, endTime - startTime, frames); + vidAvgFPS = fpsNumFrames ? ((f64)fpsNumFrames / accumDelta) : 0.f; + fpsNumFrames = 0; + accumDelta = 0.0; + snprintf(tmp, sizeof(tmp), "fps %4.1f frt %lf frm %u", vidAvgFPS, vidLastRenderTime, frames); wmAPI->set_window_title(tmp); - framesPerSec = 0; - fpsTime = endTime + 1.0; + fpsTime = endTime + vidDisplayFPSInterval; } } +f64 videoGetLastRenderTime(void) +{ + return vidLastRenderTime; +} + +f32 videoGetAverageFPS(void) +{ + return vidAvgFPS; +} + void videoClearScreen(void) { videoStartFrame(); @@ -177,19 +206,159 @@ s32 videoGetHeight(void) s32 videoGetFullscreen(void) { + vidFullscreen = wmAPI->get_fullscreen_state(); return vidFullscreen; } +s32 videoGetFullscreenMode(void) +{ + vidFullscreenExclusive = wmAPI->get_fullscreen_flag_mode(); + return vidFullscreenExclusive; +} + s32 videoGetMaximizeWindow(void) { + vidMaximize = wmAPI->get_maximized_state(); return vidMaximize; } +s32 videoGetCenterWindow(void) +{ + return vidCenter; +} + f32 videoGetAspect(void) { return gfx_current_dimensions.aspect_ratio; } +s32 videoGetDisplayModeIndex(void) +{ + for (s32 i = 1; i < vidNumModes; ++i) { + if (vidModes[i].width == gfx_current_dimensions.width && + vidModes[i].height == gfx_current_dimensions.height) { + return i; + } + } + // Current dimensions don't match any known mode, so return index 0, "Custom". + return 0; +} + +s32 videoGetMSAA(void) +{ + vidMSAA = (s32)gfx_msaa_level; + return vidMSAA; +} + +s32 videoGetVsync(void) +{ + vidVsync = wmAPI->get_swap_interval(); + return vidVsync; +} + +s32 videoGetFramerateLimit(void) +{ + vidFramerateLimit = wmAPI->get_target_fps(); + return vidFramerateLimit; +} + +s32 videoGetDisplayFPS(void) +{ + return vidDisplayFPS; +} + +static s32 videoInitDisplayModes(void) +{ + if (!wmAPI->get_current_display_mode(&vidModeDefault.width, &vidModeDefault.height)) { + vidModeDefault.width = 640; + vidModeDefault.height = 480; + return false; + } + + const s32 numBaseModes = wmAPI->get_num_display_modes(); + if (!numBaseModes) { + return false; + } + + const s32 numCustomModes = 1; + displaymode *modeList = sysMemZeroAlloc((numBaseModes + numCustomModes) * sizeof(displaymode)); + if (!modeList) { + return false; + } + + modeList[0].width = 0; + modeList[0].height = 0; + + s32 numModes = 1; + s32 w = -1, h = w, neww = w, newh = w; + + // SDL modes are guaranteed to be sorted high to low + for (s32 i = 0; i < numBaseModes; ++i) { + wmAPI->get_display_mode(i, &neww, &newh); + + if (neww != w || newh != h) { + w = neww; + h = newh; + modeList[numModes].width = w; + modeList[numModes].height = h; + ++numModes; + } + } + + modeList = sysMemRealloc(modeList, numModes * sizeof(displaymode)); + if (!modeList) { + return false; + } + + vidModes = modeList; + vidNumModes = numModes; + + return true; +} + +s32 videoGetDisplayMode(displaymode *out, const s32 index) +{ + if (index >= 0 && index < vidNumModes) { + *out = vidModes[index]; + return true; + } + return false; +} + +s32 videoGetNumDisplayModes(void) +{ + return vidNumModes; +} + +void videoSetDisplayMode(const s32 index) +{ + const displaymode dm = vidModes[index]; + + if (index == 0) { + // "Custom" video mode. + return; + } + + vidWidth = dm.width; + vidHeight = dm.height; + + s32 posX = 100; + s32 posY = 100; + if (vidCenter) { + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + } + + if (vidFullscreen) { + wmAPI->set_closest_resolution(vidWidth, vidHeight, vidCenter); + } else { + if (vidMaximize) { + videoSetMaximizeWindow(false); + } else { + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); + } + } +} + s32 videoGetTextureFilter2D(void) { return texFilter2D; @@ -215,7 +384,22 @@ void videoSetFullscreen(s32 fs) { if (fs != vidFullscreen) { vidFullscreen = !!fs; + wmAPI->set_closest_resolution(vidWidth, vidHeight, vidCenter); wmAPI->set_fullscreen(vidFullscreen); + if (!vidFullscreen && vidMaximize) { + wmAPI->set_maximize(false); + wmAPI->set_maximize(true); + } + } +} + +void videoSetFullscreenMode(s32 mode) +{ + vidFullscreenExclusive = mode; + wmAPI->set_fullscreen_flag(mode); + if (vidFullscreen) { + wmAPI->set_fullscreen(false); + wmAPI->set_fullscreen(true); } } @@ -224,6 +408,23 @@ void videoSetMaximizeWindow(s32 fs) if (fs != vidMaximize) { vidMaximize = !!fs; wmAPI->set_maximize(vidMaximize); + if (vidCenter && !vidMaximize) { + s32 posX = 0; + s32 posY = 0; + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); + } + } +} + +void videoSetCenterWindow(s32 center) +{ + vidCenter = center; + if (vidCenter && !vidMaximize) { + s32 posX = 0; + s32 posY = 0; + wmAPI->get_centered_positions(vidWidth, vidHeight, &posX, &posY); + wmAPI->set_dimensions(vidWidth, vidHeight, posX, posY); } } @@ -251,6 +452,33 @@ s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize) return gfx_create_framebuffer(w, h, upscale, autoresize); } +void videoSetMSAA(const s32 msaa) +{ + vidMSAA = msaa; + gfx_msaa_level = (u32)vidMSAA; +} + +void videoSetVsync(const s32 vsync) +{ + vidVsync = wmAPI->set_swap_interval(vsync) ? vsync : 0; + + if (vidVsync == 0 && vidFramerateLimit == 0) { + // cap FPS if there's no vsync to prevent the game from exploding + videoSetFramerateLimit(VIDEO_MAX_FPS); + } +} + +void videoSetFramerateLimit(const s32 limit) +{ + vidFramerateLimit = (vidVsync == 0 && limit == 0) ? VIDEO_MAX_FPS : limit; + wmAPI->set_target_fps(vidFramerateLimit); +} + +void videoSetDisplayFPS(const s32 displayfps) +{ + vidDisplayFPS = displayfps; +} + void videoSetFramebuffer(s32 target) { return gfx_set_framebuffer(target, 1.f); @@ -287,6 +515,11 @@ void videoFreeCachedTexture(const void *texptr) gfx_texture_cache_delete(texptr); } +void videoShutdown(void) +{ + free(vidModes); +} + PD_CONSTRUCTOR static void videoConfigInit(void) { configRegisterInt("Video.DefaultFullscreen", &vidFullscreen, 0, 1); @@ -294,10 +527,13 @@ PD_CONSTRUCTOR static void videoConfigInit(void) configRegisterInt("Video.DefaultWidth", &vidWidth, 0, 32767); configRegisterInt("Video.DefaultHeight", &vidHeight, 0, 32767); configRegisterInt("Video.ExclusiveFullscreen", &vidFullscreenExclusive, 0, 1); + configRegisterInt("Video.CenterWindow", &vidCenter, 0, 1); configRegisterInt("Video.AllowHiDpi", &vidAllowHiDpi, 0, 1); configRegisterInt("Video.VSync", &vidVsync, -1, 10); configRegisterInt("Video.FramebufferEffects", &vidFramebuffers, 0, 1); configRegisterInt("Video.FramerateLimit", &vidFramerateLimit, 0, VIDEO_MAX_FPS); + configRegisterInt("Video.DisplayFPS", &vidDisplayFPS, 0, 1); + configRegisterFloat("Video.DisplayFPSInterval", &vidDisplayFPSInterval, 0.01f, 32.f); configRegisterInt("Video.MSAA", &vidMSAA, 1, 16); configRegisterInt("Video.TextureFilter", &texFilter, 0, 2); configRegisterInt("Video.TextureFilter2D", &texFilter2D, 0, 1); diff --git a/src/game/bondmove.c b/src/game/bondmove.c index 552dd0f83..1a5dcb6a8 100644 --- a/src/game/bondmove.c +++ b/src/game/bondmove.c @@ -1810,7 +1810,9 @@ void bmoveProcessInput(bool allowc1x, bool allowc1y, bool allowc1buttons, bool i g_Vars.currentplayer->bondactivateorreload = 0; + s32 usereloads = (controlmode != CONTROLMODE_PC); #ifndef PLATFORM_N64 + usereloads = usereloads || PLAYER_EXTCFG().usereloads; if (controlmode == CONTROLMODE_PC && movedata.alt1tapcount) { g_Vars.currentplayer->bondactivateorreload = g_Vars.currentplayer->bondactivateorreload | JO_ACTION_RELOAD; } @@ -1818,7 +1820,7 @@ void bmoveProcessInput(bool allowc1x, bool allowc1y, bool allowc1buttons, bool i if (movedata.btapcount) { g_Vars.currentplayer->activatetimelast = g_Vars.currentplayer->activatetimethis; g_Vars.currentplayer->activatetimethis = g_Vars.lvframe60; - if (controlmode == CONTROLMODE_PC) { + if (!usereloads) { g_Vars.currentplayer->bondactivateorreload = g_Vars.currentplayer->bondactivateorreload | JO_ACTION_ACTIVATE; } else { g_Vars.currentplayer->bondactivateorreload = movedata.btapcount ? diff --git a/src/game/challenge.c b/src/game/challenge.c index ca809ca99..8c738105f 100644 --- a/src/game/challenge.c +++ b/src/game/challenge.c @@ -687,6 +687,12 @@ void challengeApply(void) for (i = 0; i < MAX_PLAYERS; i++) { g_PlayerConfigsArray[i].base.team = 0; } + +#ifndef PLATFORM_N64 + // Enable Friendly Fire option + g_MpSetup.options |= MPOPTION_FRIENDLYFIRE; +#endif + } s32 challengeRemovePlayerLock(void) diff --git a/src/game/lv.c b/src/game/lv.c index a807dc0d5..53f2abe46 100644 --- a/src/game/lv.c +++ b/src/game/lv.c @@ -98,6 +98,7 @@ #include "types.h" #ifndef PLATFORM_N64 #include "debugger.h" +#include "video.h" #endif struct sndstate *g_MiscSfxAudioHandles[3]; @@ -956,6 +957,45 @@ void lvFindThreats(void) } } +#ifndef PLATFORM_N64 +Gfx *lvRenderFPS(Gfx *gdl) +{ + const f32 fps = videoGetAverageFPS(); + const u8 a = 160; + s32 x = 27, y = 13; + u32 color; + char buffer[16]; + + if (fps <= 30.f) { + // red -> yellow + color = 0xff000000 | a | ((u32)((fps / 30.f) * 255.f) << 16); + } else if (fps <= 60.f) { + // yellow -> green + color = 0x00ff0000 | a | ((u32)((1.f - (fps - 30.f) / 30.f) * 255.f) << 24); + } else if (fps <= 90.f) { + // green -> cyan + color = 0x00ff0000 | a | ((u32)(((fps - 60.f) / 30.f) * 255.f) << 8); + } else { + // cyan + color = 0x00ffff00 | a; + } + + if (g_CharsNumeric && g_FontNumeric) { + snprintf(buffer, sizeof buffer, "%.2f", fps); + + gSPSetExtraGeometryModeEXT(gdl++, g_HudAlignModeL); + + gdl = text0f153628(gdl); + gdl = textRender(gdl, &x, &y, buffer, g_CharsNumeric, g_FontNumeric, color, 0x000000a0, viGetWidth(), viGetHeight(), 0, 0); + gdl = text0f153780(gdl); + + gSPClearExtraGeometryModeEXT(gdl++, g_HudAlignModeL); + } + + return gdl; +} +#endif + /** * Renders a complete frame for all players, and also does some other game logic * that really doesn't belong here. @@ -1309,7 +1349,7 @@ Gfx *lvRender(Gfx *gdl) if (!currentPlayerInteract(false)) { #ifndef PLATFORM_N64 // n64 behavior: interact sucessful, cancel reload - if (!PLAYER_EXTCFG().extcontrols) { + if (!PLAYER_EXTCFG().extcontrols || PLAYER_EXTCFG().usereloads) { g_Vars.currentplayer->bondactivateorreload = (g_Vars.currentplayer->bondactivateorreload & ~JO_ACTION_RELOAD); } #endif @@ -1751,6 +1791,12 @@ Gfx *lvRender(Gfx *gdl) gDPSetScissor(gdl++, G_SC_NON_INTERLACE, 0, 0, viGetWidth(), viGetHeight()); +#ifndef PLATFORM_N64 + if (videoGetDisplayFPS()) { + gdl = lvRenderFPS(gdl); + } +#endif + #if VERSION < VERSION_NTSC_1_0 if ((uintptr_t)gdl < (uintptr_t)g_GfxBuffers[g_GfxActiveBufferIndex] || (uintptr_t)gdl > (uintptr_t)g_GfxBuffers[g_GfxActiveBufferIndex + 1]) { diff --git a/src/game/menu.c b/src/game/menu.c index 970b62ddb..1e9159cae 100644 --- a/src/game/menu.c +++ b/src/game/menu.c @@ -142,6 +142,9 @@ s32 g_MpPlayerNum = 0; s32 g_MenuMouseControl = true; s32 g_MenuUsingMouse = false; s32 g_MenuKeyboardPlayer = -1; +s32 g_AllowMouseHeld = true; +s32 g_MouseDimmedMode = false; +s32 g_MouseEndDeferredSlider = false; #endif void menuPlaySound(s32 menusound) @@ -1512,6 +1515,12 @@ void menuOpenDialog(struct menudialogdef *dialogdef, struct menudialog *dialog, void menuPushDialog(struct menudialogdef *dialogdef) { +#ifndef PLATFORM_N64 + // Prevent the mouse from immediately changing a slider's value, as was + // happening while entering the Audio menu. + g_AllowMouseHeld = false; +#endif + if (dialogdef) { menuUnsetModel(&g_Menus[g_MpPlayerNum].menumodel); @@ -4700,6 +4709,7 @@ void menuProcessInput(void) inputs.back2 = 0; #ifndef PLATFORM_N64 + inputs.mouseheld = false; inputs.mousemoved = false; inputs.mousescroll = 0; inputs.mousex = 0; @@ -4709,8 +4719,16 @@ void menuProcessInput(void) // ESC always acts as back inputs.back = inputKeyJustPressed(VK_ESCAPE); if (inputMouseIsEnabled() && !inputMouseIsLocked() && g_MenuMouseControl) { - inputs.mousemoved = inputMouseGetPosition(&inputs.mousex, &inputs.mousey); + inputs.mouseheld = inputKeyPressed(VK_MOUSE_LEFT); + if (!inputs.mouseheld) { + g_AllowMouseHeld = true; + if (g_MouseDimmedMode) { + g_MouseDimmedMode = false; + g_MouseEndDeferredSlider = true; + } + } inputs.mousescroll = inputKeyPressed(VK_MOUSE_WHEEL_DN) - inputKeyPressed(VK_MOUSE_WHEEL_UP); + inputs.mousemoved = inputMouseGetPosition(&inputs.mousex, &inputs.mousey) || inputs.mousescroll; // aspect correct the X const f32 cx = ((f32)inputs.mousex - (f32)(SCREEN_WIDTH_LO / 2)) * (videoGetAspect() / SCREEN_ASPECT); inputs.mousex = (f32)(SCREEN_WIDTH_LO / 2) + cx; diff --git a/src/game/menuitem.c b/src/game/menuitem.c index 7bf3a37ea..72c24573a 100644 --- a/src/game/menuitem.c +++ b/src/game/menuitem.c @@ -50,6 +50,8 @@ u8 g_KeyboardKeys[5][10] = { { '1','2','1','2','1','2','3','1','2','3' }, }; +static s32 deferredindex = -1; + s32 func0f0e5ce0(s32 value) { if (value < var800711a4) { @@ -2266,7 +2268,7 @@ Gfx* menuitemColorBoxRender(Gfx *gdl, struct menurendercontext *context) return gdl; } -#endif +#endif Gfx *menuitemSelectableRender(Gfx *gdl, struct menurendercontext *context) { @@ -2427,8 +2429,15 @@ Gfx *menuitemSliderRender(Gfx *gdl, struct menurendercontext *context) extray = 0; if (context->item->handler != NULL) { - context->item->handler(MENUOP_GETSLIDER, context->item, &data); - slidervalue = (s16) data.slider.value; + if ((context->item->flags & MENUITEMFLAG_SLIDER_DEFERRED) && + deferredindex != -1 && + context->dialog->dimmed && + context->focused) { + slidervalue = (s16) deferredindex; + } else { + context->item->handler(MENUOP_GETSLIDER, context->item, &data); + slidervalue = (s16) data.slider.value; + } } else { slidervalue = 0; } @@ -2550,7 +2559,7 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct if ((tickflags & MENUTICKFLAG_ITEMISFOCUSED)) { #ifndef PLATFORM_N64 - if (g_MenuUsingMouse && inputs->select) { + if (g_AllowMouseHeld && g_MenuUsingMouse && (inputs->select || inputs->mouseheld)) { // handle mouse struct menudialog *dialog = g_Menus[g_MpPlayerNum].curdialog; if (dialog) { @@ -2559,19 +2568,27 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct const s32 size = right - left; const s32 delta = inputs->mousex - left; if (delta >= -8 && delta <= size + 8) { - index = (delta / (f32)size) * item->param3; - if (index < 0) { - index = 0; + if ((tickflags & MENUTICKFLAG_DIALOGISDIMMED) == 0) { + g_MouseDimmedMode = true; + } else { + index = (delta / (f32)size) * item->param3; + if (index < 0) { + index = 0; + } + if (index > item->param3) { + index = item->param3; + } + if (item->handler) { + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + deferredindex = index; + } else { + item->handler(MENUOP_GET, item, &handlerdata); + handlerdata.slider.value = index; + item->handler(MENUOP_SET, item, &handlerdata); + } + } + return true; } - if (index > item->param3) { - index = item->param3; - } - if (item->handler) { - item->handler(MENUOP_GET, item, &handlerdata); - handlerdata.slider.value = index; - item->handler(MENUOP_SET, item, &handlerdata); - } - return true; } } } @@ -2579,12 +2596,32 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct if (tickflags & MENUTICKFLAG_DIALOGISDIMMED) { if (item->handler) { - item->handler(MENUOP_GETSLIDER, item, &handlerdata); - index = (s16) handlerdata.slider.value; + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + if (deferredindex == -1) { + item->handler(MENUOP_GETSLIDER, item, &handlerdata); + deferredindex = (s16) handlerdata.slider.value; + } + index = deferredindex; + } else { + item->handler(MENUOP_GETSLIDER, item, &handlerdata); + index = (s16) handlerdata.slider.value; + } + } else { index = 0; } +#ifndef PLATFORM_N64 + if (g_MenuUsingMouse && inputs->mousescroll) { + if ((item->flags & MENUITEMFLAG_SLIDER_FAST) == 0) { + index += -inputs->mousescroll; + } else { + s32 mult = item->param3 / 40; + index += -inputs->mousescroll * (mult ? mult : 1); + } + } +#endif + if ((item->flags & MENUITEMFLAG_SLIDER_FAST) == 0 && g_Menus[g_MpPlayerNum].xrepeatmode == MENUREPEATMODE_SLOW) { index = index + inputs->leftright; @@ -2660,11 +2697,22 @@ bool menuitemSliderTick(struct menuitem *item, struct menudialog *dialog, struct inputs->leftright = 0; handlerdata.slider.value = index; - if (item->handler) { - item->handler(MENUOP_SET, item, &handlerdata); + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + deferredindex = index; + } else { + if (item->handler) { + item->handler(MENUOP_SET, item, &handlerdata); + } } - if (inputs->select) { + if (inputs->select || g_MouseEndDeferredSlider) { + g_MouseEndDeferredSlider = false; + if (item->flags & MENUITEMFLAG_SLIDER_DEFERRED) { + deferredindex = -1; + if (item->handler) { + item->handler(MENUOP_SET, item, &handlerdata); + } + } dialog->dimmed = false; } } else { @@ -2704,7 +2752,7 @@ Gfx *menuitemCarouselRender(Gfx *gdl, struct menurendercontext *context) #ifdef PLATFORM_N64 s16 chevronOffset = 0; -#else +#else s16 chevronOffset = 3; #endif diff --git a/src/game/mplayer/mplayer.c b/src/game/mplayer/mplayer.c index df78ced21..b329f51db 100644 --- a/src/game/mplayer/mplayer.c +++ b/src/game/mplayer/mplayer.c @@ -48,6 +48,11 @@ struct modeldef *var800acc28[18]; struct mpweaponset g_MpWeaponSets[12]; s32 g_MpWeaponSetNum; +#ifndef PLATFORM_N64 +u8 g_MpWeaponSetRandomFilters[NUM_MPWEAPONS]; +s32 g_MpWeaponRandomFilterNum; +#endif + #if VERSION >= VERSION_NTSC_1_0 const char var7f1b8a00[] = "||||||||||||| Starting game... players %d\n"; #endif @@ -130,6 +135,7 @@ struct mpweapon g_MpWeapons[NUM_MPWEAPONS] = { .crosshaircolour = 0x00ff0028, \ .crosshairsize = 2, \ .crosshairhealth = CROSSHAIR_HEALTH_OFF, \ + .usereloads = false, \ } struct extplayerconfig g_PlayerExtCfg[MAX_PLAYERS] = { @@ -507,6 +513,12 @@ void mpPlayerSetDefaults(s32 playernum, bool autonames) for (i = 0; i < ARRAYCOUNT(g_PlayerConfigsArray); i++) { g_PlayerConfigsArray[playernum].gunfuncs[i] = 0; } + +#ifndef PLATFORM_N64 + for (i = 0; i < ARRAYCOUNT(g_MpWeapons); i++) { + g_MpWeaponSetRandomFilters[i] = 1; + } +#endif } void func0f1881d4(s32 index) @@ -1157,10 +1169,40 @@ void func0f18913c(void) } } +#ifndef PLATFORM_N64 +void mpSetRandomWeapons(u8 weapons[]) +{ + s32 lockcount = 0; + s32 index = 0; + s32 i; + + for (i = 0; i < NUM_MPWEAPONS; i++) { + if (challengeIsFeatureUnlocked(g_MpWeapons[i].unlockfeature)) { + if (g_MpWeaponSetRandomFilters[i] == 1) { + weapons[index] = i - lockcount; + index++; + } + } else { + lockcount++; + } + } + + if (index == 0) { + weapons[0] = 0; // optionindex (shifted by unlocked weapons, but usually 0 is "Nothing") + g_MpWeaponRandomFilterNum = 1; + } else { + g_MpWeaponRandomFilterNum = index; + } +} +#endif + void mpApplyWeaponSet(void) { s32 i; u8 *ptr; +#ifndef PLATFORM_N64 + u8 randomweapons[NUM_MPWEAPONS]; +#endif if (g_MpWeaponSetNum >= 0 && g_MpWeaponSetNum < ARRAYCOUNT(g_MpWeaponSets)) { if (challengeIsFeatureUnlocked(g_MpWeaponSets[g_MpWeaponSetNum].requirefeatures[0]) @@ -1198,17 +1240,31 @@ void mpApplyWeaponSet(void) } } } else if (g_MpWeaponSetNum == WEAPONSET_RANDOM) { +#ifdef PLATFORM_N64 s32 numoptions = mpGetNumWeaponOptions(); for (i = 0; i < ARRAYCOUNT(g_MpSetup.weapons); i++) { mpSetWeaponSlot(i, rngRandom() % numoptions); } +#else + mpSetRandomWeapons(randomweapons); + for (i = 0; i < ARRAYCOUNT(g_MpSetup.weapons); i++) { + mpSetWeaponSlot(i, randomweapons[rngRandom() % g_MpWeaponRandomFilterNum]); + } +#endif } else if (g_MpWeaponSetNum == WEAPONSET_RANDOMFIVE) { +#ifdef PLATFORM_N64 s32 numoptions = mpGetNumWeaponOptions() - 2; for (i = 0; i < 5; i++) { mpSetWeaponSlot(i, rngRandom() % numoptions + 1); } +#else + mpSetRandomWeapons(randomweapons); + for (i = 0; i < 5; i++) { + mpSetWeaponSlot(i, randomweapons[rngRandom() % g_MpWeaponRandomFilterNum]); + } +#endif mpSetWeaponSlot(i, mpGetNumWeaponOptions() - 1); } diff --git a/src/game/mplayer/setup.c b/src/game/mplayer/setup.c index 380c1b283..cae26d3c7 100644 --- a/src/game/mplayer/setup.c +++ b/src/game/mplayer/setup.c @@ -1165,6 +1165,155 @@ struct menudialogdef g_MpSaveSetupExistsMenuDialog = { }; #ifndef PLATFORM_N64 +MenuItemHandlerResult mpSelectRandomWeaponListHandler(s32 operation, struct menuitem *item, union handlerdata *data) +{ + static const char *labels[] = { + "Select Dark", + "Select Classic", + "Select All", + "Select None", + }; + + switch (operation) { + case MENUOP_GETOPTIONCOUNT: + data->list.value = mpGetNumWeaponOptions() + 4; + break; + case MENUOP_GETOPTIONTEXT: + { + s32 numweapons = mpGetNumWeaponOptions(); + + if (data->list.value < numweapons) { + return (uintptr_t) mpGetWeaponLabel(data->list.value); + } else { + return (intptr_t)labels[data->list.value - numweapons]; + } + } + case MENUOP_SET: + { + s32 numweapons = mpGetNumWeaponOptions(); + s32 mpweaponnum = data->list.value; + s32 optionindex = mpweaponnum; + s32 i; + + if (data->list.value < numweapons) { + if (data->list.unk04 == 0) { + for (i = 0; i <= mpweaponnum; i++) { + if (challengeIsFeatureUnlocked(g_MpWeapons[i].unlockfeature) == 0) { + mpweaponnum++; + } + + optionindex = mpweaponnum; + } + + g_MpWeaponSetRandomFilters[optionindex] = 1 - g_MpWeaponSetRandomFilters[optionindex]; + } + } else { + s32 index = data->list.value - numweapons; + + switch (index) { + case 0: + // Select Dark + for (i = 0; i < ARRAYCOUNT(g_MpWeapons); i++) { + if ((i >= MPWEAPON_NONE && i <= MPWEAPON_COMBATBOOST) || i >= MPWEAPON_SHIELD) { + g_MpWeaponSetRandomFilters[i] = 1; + } else { + g_MpWeaponSetRandomFilters[i] = 0; + } + } + break; + case 1: + // Select Classic + for (i = 0; i < ARRAYCOUNT(g_MpWeapons); i++) { + if (i >= MPWEAPON_PP9I && i <= MPWEAPON_RCP45) { + g_MpWeaponSetRandomFilters[i] = 1; + } else { + g_MpWeaponSetRandomFilters[i] = 0; + } + } + break; + case 2: + // Select All + for (i = 0; i < ARRAYCOUNT(g_MpWeapons); i++) { + g_MpWeaponSetRandomFilters[i] = 1; + } + break; + case 3: + // Select None + for (i = 0; i < ARRAYCOUNT(g_MpWeapons); i++) { + g_MpWeaponSetRandomFilters[i] = 0; + } + break; + } + } + } + break; + case MENUOP_GETSELECTEDINDEX: + data->list.value = 0x000fffff; + break; + case MENUOP_GETLISTITEMCHECKBOX: + { + s32 numweapons = mpGetNumWeaponOptions(); + s32 mpweaponnum = data->list.value; + s32 optionindex = mpweaponnum; + s32 i; + + if (data->list.value < numweapons) { + + for (i = 0; i <= mpweaponnum; i++) { + if (challengeIsFeatureUnlocked(g_MpWeapons[i].unlockfeature) == 0) { + mpweaponnum++; + } + + optionindex = mpweaponnum; + } + + data->list.unk04 = g_MpWeaponSetRandomFilters[optionindex]; + } + } + break; + } + + return 0; +} + +struct menuitem g_MpSelectRandomWeaponsMenuItems[] = { + { + MENUITEMTYPE_LIST, + 0, + MENUITEMFLAG_LOCKABLEMINOR, + 0x00000078, + 0x0000004d, + mpSelectRandomWeaponListHandler, + }, + { MENUITEMTYPE_END }, +}; + +struct menudialogdef g_MpSelectRandomWeaponsMenuDialog = { + MENUDIALOGTYPE_DEFAULT, + (uintptr_t)"Select Weapons", + g_MpSelectRandomWeaponsMenuItems, + NULL, + MENUDIALOGFLAG_LITERAL_TEXT, + NULL, +}; + +MenuItemHandlerResult menuhandlerMpSelectRandomWeapons(s32 operation, struct menuitem *item, union handlerdata *data) +{ + switch (operation) { + case MENUOP_CHECKDISABLED: + case MENUOP_CHECKHIDDEN: + if (g_MpWeaponSetNum == WEAPONSET_RANDOM + || g_MpWeaponSetNum == WEAPONSET_RANDOMFIVE) { + return false; + } + return true; + case MENUOP_SET: + menuPushDialog(&g_MpSelectRandomWeaponsMenuDialog); + } + + return 0; +} + MenuItemHandlerResult menuhandlerMpAutoRandomWeapon(s32 operation, struct menuitem *item, union handlerdata *data) { static const char *labels[] = { @@ -1220,6 +1369,14 @@ struct menuitem g_MpWeaponsMenuItems[] = { menuhandlerMpWeaponSetDropdown, }, #ifndef PLATFORM_N64 + { + MENUITEMTYPE_SELECTABLE, + 0, + MENUITEMFLAG_LITERAL_TEXT, + (uintptr_t)"Select Weapons\n", + 0, + menuhandlerMpSelectRandomWeapons, + }, { MENUITEMTYPE_DROPDOWN, 0, diff --git a/src/include/constants.h b/src/include/constants.h index fcfbdec85..f73736a8f 100644 --- a/src/include/constants.h +++ b/src/include/constants.h @@ -1666,6 +1666,7 @@ #define MENUITEMFLAG_CAROUSEL_04000000 0x04000000 #define MENUITEMFLAG_LITERAL_TEXT 0x08000000 #define MENUITEMFLAG_SLIDER_WIDE 0x10000000 +#define MENUITEMFLAG_SLIDER_DEFERRED 0x20000000 #define MENUITEMTYPE_LABEL 0x01 #define MENUITEMTYPE_LIST 0x02 @@ -1696,7 +1697,7 @@ #ifndef PLATFORM_N64 #define MENUITEMTYPE_COLORBOX 0x1b -#endif +#endif #define MENUMODELFLAG_HASSCALE 0x01 #define MENUMODELFLAG_HASPOSITION 0x02 diff --git a/src/include/data.h b/src/include/data.h index 07086603f..0bc0f12b3 100644 --- a/src/include/data.h +++ b/src/include/data.h @@ -539,6 +539,9 @@ extern const struct weathercfg *g_CurWeatherConfig; extern s32 g_MenuUsingMouse; extern s32 g_MenuKeyboardPlayer; +extern s32 g_AllowMouseHeld; +extern s32 g_MouseDimmedMode; +extern s32 g_MouseEndDeferredSlider; extern f32 g_ViShakeIntensityMult; extern u32 g_TexFilter2D; @@ -556,6 +559,9 @@ extern s32 g_MusicDisableMpDeath; extern s32 g_BgunGeMuzzleFlashes; extern s32 g_FileAutoSelect; +extern u8 g_MpWeaponSetRandomFilters[NUM_MPWEAPONS]; +extern s32 g_MpWeaponRandomFilterNum; + #define PLAYER_EXTCFG() g_PlayerExtCfg[g_Vars.currentplayerstats->mpindex & 3] #define PLAYER_DEFAULT_FOV (PLAYER_EXTCFG().fovy) diff --git a/src/include/game/mplayer/mplayer.h b/src/include/game/mplayer/mplayer.h index b2ab42f95..3d2591ca7 100644 --- a/src/include/game/mplayer/mplayer.h +++ b/src/include/game/mplayer/mplayer.h @@ -34,6 +34,9 @@ s32 func0f189058(bool full); s32 func0f189088(void); char *mpGetWeaponSetName(s32 arg0); void func0f18913c(void); +#ifndef PLATFORM_N64 +void mpSetRandomWeapons(u8 weapons[]); +#endif void mpApplyWeaponSet(void); void mpSetWeaponSet(s32 weaponsetnum); void func0f1895e8(void); diff --git a/src/include/types.h b/src/include/types.h index 79f553fc9..1da61867b 100644 --- a/src/include/types.h +++ b/src/include/types.h @@ -4996,8 +4996,9 @@ struct menuinputs { /*0x10*/ s32 unk10; /*0x14*/ u8 unk14; #ifndef PLATFORM_N64 - /*0x15*/ u8 mousemoved; - /*0x16*/ s8 mousescroll; + /*0x15*/ u8 mouseheld; + /*0x16*/ u8 mousemoved; + /*0x17*/ s8 mousescroll; /*0x18*/ s32 mousex; /*0x1c*/ s32 mousey; #endif @@ -6152,6 +6153,7 @@ struct extplayerconfig { u32 crosshaircolour; u32 crosshairsize; s32 crosshairhealth; + s32 usereloads; }; #endif