RingRacers/src/menus/options-profiles-edit-controls.c
toaster 224deed01d More copyright updates
- TehRealSalt and Lat` are currently preoccupied, so handle their credits
- Correct some accidential copypastes of existing boilerplate into new files
- Add a handful more of mine
- Consistency for Kaito Sinclaire's online handle
2024-04-02 22:14:49 +01:00

496 lines
13 KiB
C

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 2024 by James Robert Roman.
// Copyright (C) 2024 by "Lat'".
// Copyright (C) 2024 by Kart Krew.
//
// This program is free software distributed under the
// terms of the GNU General Public License, version 2.
// See the 'LICENSE' file for more details.
//-----------------------------------------------------------------------------
/// \file menus/options-profiles-edit-controls.c
/// \brief Profile Controls Editor
#include "../g_input.h"
#include "../k_menu.h"
#include "../s_sound.h"
#include "../i_joy.h" // for joystick menu controls
menuitem_t OPTIONS_ProfileControls[] = {
{IT_HEADER, "MAIN CONTROLS", "That's the stuff on the controller!!",
NULL, {NULL}, 0, 0},
{IT_CONTROL, "Accel / Confirm", "Accelerate / Confirm",
"TLB_A", {.routine = M_ProfileSetControl}, gc_a, 0},
{IT_CONTROL, "Look back", "Look backwards / Go back",
"TLB_B", {.routine = M_ProfileSetControl}, gc_b, 0},
{IT_CONTROL, "Spin Dash", "Spin Dash / Extra",
"TLB_C", {.routine = M_ProfileSetControl}, gc_c, 0},
{IT_CONTROL, "Brake / Go back", "Brake / Go back",
"TLB_D", {.routine = M_ProfileSetControl}, gc_x, 0},
{IT_CONTROL, "Respawn", "Respawn",
"TLB_E", {.routine = M_ProfileSetControl}, gc_y, 0},
{IT_CONTROL, "Action", "Multiplayer quick-chat / quick-vote",
"TLB_F", {.routine = M_ProfileSetControl}, gc_z, 0},
{IT_CONTROL, "Use Item", "Use item",
"TLB_H", {.routine = M_ProfileSetControl}, gc_l, 0},
{IT_CONTROL, "Drift", "Drift",
"TLB_I", {.routine = M_ProfileSetControl}, gc_r, 0},
{IT_CONTROL, "Turn Left", "Turn left",
"TLB_M", {.routine = M_ProfileSetControl}, gc_left, 0},
{IT_CONTROL, "Turn Right", "Turn right",
"TLB_L", {.routine = M_ProfileSetControl}, gc_right, 0},
{IT_CONTROL, "Aim Forward", "Aim forwards",
"TLB_J", {.routine = M_ProfileSetControl}, gc_up, 0},
{IT_CONTROL, "Aim Backwards", "Aim backwards",
"TLB_K", {.routine = M_ProfileSetControl}, gc_down, 0},
{IT_CONTROL, "Open pause menu", "Open pause menu",
"TLB_G", {.routine = M_ProfileSetControl}, gc_start, 0},
{IT_HEADER, "OPTIONAL CONTROLS", "Take a screenshot, chat...",
NULL, {NULL}, 0, 0},
{IT_CONTROL, "SCREENSHOT", "Take a high resolution screenshot.",
NULL, {.routine = M_ProfileSetControl}, gc_screenshot, 0},
{IT_CONTROL, "RECORD VIDEO", "Record a video with sound.",
NULL, {.routine = M_ProfileSetControl}, gc_startmovie, 0},
{IT_CONTROL, "RECORD GIF", "Record a pixel perfect GIF.",
NULL, {.routine = M_ProfileSetControl}, gc_startlossless, 0},
{IT_CONTROL, "SHOW RANKINGS", "Display the current rankings mid-game.",
NULL, {.routine = M_ProfileSetControl}, gc_rankings, 0},
{IT_CONTROL, "OPEN CHAT", "Opens full keyboard chatting for online games.",
NULL, {.routine = M_ProfileSetControl}, gc_talk, 0},
{IT_CONTROL, "OPEN TEAM CHAT", "Opens team-only full chat for online games.",
NULL, {.routine = M_ProfileSetControl}, gc_teamtalk, 0},
{IT_CONTROL, "LUA/A", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luaa, 0},
{IT_CONTROL, "LUA/B", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luab, 0},
{IT_CONTROL, "LUA/C", "May be used by add-ons.",
NULL, {.routine = M_ProfileSetControl}, gc_luac, 0},
{IT_CONTROL, "OPEN CONSOLE", "Opens the developer options console.",
NULL, {.routine = M_ProfileSetControl}, gc_console, 0},
{IT_HEADER, "TEST AND CONFIRM", "",
NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "TRY MAPPINGS", "Test your controls.",
NULL, {.routine = M_ProfileTryController}, 0, 0},
{IT_STRING | IT_CALL, "RESET TO DEFAULT", "Reset all controls back to default.",
NULL, {.routine = M_ProfileDefaultControls}, 0, 0},
{IT_STRING | IT_CALL, "CLEAR ALL", "Unbind all controls.",
NULL, {.routine = M_ProfileClearControls}, 0, 0},
{IT_STRING | IT_CALL, "CONFIRM", "Go back to profile setup.",
NULL, {.routine = M_ProfileControlsConfirm}, 0, 0},
};
menu_t OPTIONS_ProfileControlsDef = {
sizeof (OPTIONS_ProfileControls) / sizeof (menuitem_t),
&OPTIONS_EditProfileDef,
0,
OPTIONS_ProfileControls,
32, 80,
SKINCOLOR_ULTRAMARINE, 0,
MBF_DRAWBGWHILEPLAYING,
"FILE",
3, 5,
M_DrawProfileControls,
M_DrawOptionsCogs,
M_HandleProfileControls,
NULL,
NULL,
M_ProfileControlsInputs,
};
// sets whatever device has had its key pressed to the active device.
// 20/05/22: Commented out for now but not deleted as it might still find some use in the future?
/*
static void SetDeviceOnPress(void)
{
UINT8 i;
for (i=0; i < MAXDEVICES; i++)
{
if (deviceResponding[i])
{
G_SetDeviceForPlayer(0, i); // Force-set this joystick as the current joystick we're using for P1 (which is the only one controlling menus)
CONS_Printf("SetDeviceOnPress: Device for %d set to %d\n", 0, i);
return;
}
}
}
*/
static boolean M_ClearCurrentControl(void)
{
// check if we're on a valid menu option...
if (currentMenu->menuitems[itemOn].mvar1)
{
// clear controls for that key
INT32 i;
for (i = 0; i < MAXINPUTMAPPING; i++)
optionsmenu.tempcontrols[currentMenu->menuitems[itemOn].mvar1][i] = KEY_NULL;
return true;
}
return false;
}
void M_HandleProfileControls(void)
{
const UINT8 pid = 0;
UINT8 maxscroll = currentMenu->numitems - 5;
M_OptionsTick();
optionsmenu.contx += (optionsmenu.tcontx - optionsmenu.contx)/2;
optionsmenu.conty += (optionsmenu.tconty - optionsmenu.conty)/2;
if (abs(optionsmenu.contx - optionsmenu.tcontx) < 2 && abs(optionsmenu.conty - optionsmenu.tconty) < 2)
{
optionsmenu.contx = optionsmenu.tcontx;
optionsmenu.conty = optionsmenu.tconty; // Avoid awkward 1 px errors.
}
optionsmenu.controlscroll = itemOn - 3; // very barebones scrolling, but it works just fine for our purpose.
if (optionsmenu.controlscroll > maxscroll)
optionsmenu.controlscroll = maxscroll;
if (optionsmenu.controlscroll < 0)
optionsmenu.controlscroll = 0;
// bindings, cancel if timer is depleted.
if (optionsmenu.bindtimer)
{
if (optionsmenu.bindtimer > 0)
optionsmenu.bindtimer--;
}
else if (currentMenu->menuitems[itemOn].mvar1) // check if we're on a valid menu option...
{
// Hold right to begin clearing the control.
//
// If bindben timer increases enough, bindben_swallow
// will be set.
// This is a commitment to clear the control.
// You can keep holding right to postpone the clear
// but once you let go, you are locked out of
// pressing it again until the animation finishes.
if (menucmd[pid].dpad_lr > 0 && (optionsmenu.bindben || !optionsmenu.bindben_swallow))
{
optionsmenu.bindben++;
}
else
{
optionsmenu.bindben = 0;
if (optionsmenu.bindben_swallow)
{
optionsmenu.bindben_swallow--;
if (optionsmenu.bindben_swallow == 100) // special countdown for the "quick" animation
optionsmenu.bindben_swallow = 0;
else if (!optionsmenu.bindben_swallow) // long animation, clears control when done
M_ClearCurrentControl();
}
}
}
}
void M_ProfileTryController(INT32 choice)
{
(void)choice;
optionsmenu.trycontroller = TICRATE*5;
// Apply these controls right now on P1's end.
G_ApplyControlScheme(0, optionsmenu.tempcontrols);
}
static void M_ProfileControlSaveResponse(INT32 choice)
{
if (choice == MA_YES)
{
SINT8 belongsto = PR_ProfileUsedBy(optionsmenu.profile);
// Save the profile
memcpy(&optionsmenu.profile->controls, optionsmenu.tempcontrols, sizeof(gamecontroldefault));
// If this profile is in-use by anyone, apply the changes immediately upon exiting.
// Don't apply the profile itself as that would lead to issues mid-game.
if (belongsto > -1 && belongsto < MAXSPLITSCREENPLAYERS)
{
G_ApplyControlScheme(belongsto, optionsmenu.tempcontrols);
}
}
else
{
// Revert changes
memcpy(optionsmenu.tempcontrols, optionsmenu.profile->controls, sizeof(gamecontroldefault));
}
M_GoBack(0);
}
void M_ProfileControlsConfirm(INT32 choice)
{
if (!memcmp(optionsmenu.profile->controls, optionsmenu.tempcontrols, sizeof(gamecontroldefault)))
{
M_GoBack(0); // no change
}
else if (choice == 0)
{
M_StartMessage(
"Profiles",
"You have unsaved changes to your controls.\n"
"Please confirm if you wish to save them.\n",
&M_ProfileControlSaveResponse,
MM_YESNO,
NULL,
NULL
);
}
else
M_ProfileControlSaveResponse(MA_YES);
// Reapply player 1's real profile.
if (cv_currprofile.value > -1)
{
PR_ApplyProfile(cv_lastprofile[0].value, 0);
}
}
boolean M_ProfileControlsInputs(INT32 ch)
{
const UINT8 pid = 0;
(void)ch;
// By default, accept all inputs.
if (optionsmenu.trycontroller)
{
if (menucmd[pid].dpad_ud || menucmd[pid].dpad_lr || menucmd[pid].buttons)
{
if (menucmd[pid].dpad_ud != menucmd[pid].prev_dpad_ud || menucmd[pid].dpad_lr != menucmd[pid].prev_dpad_lr)
S_StartSound(NULL, sfx_s3k5b);
UINT32 newbuttons = menucmd[pid].buttons & ~(menucmd[pid].buttonsHeld);
if (newbuttons & MBT_L)
S_StartSound(NULL, sfx_kc69);
if (newbuttons & MBT_R)
S_StartSound(NULL, sfx_s3ka2);
if (newbuttons & MBT_A)
S_StartSound(NULL, sfx_kc3c);
if (newbuttons & MBT_B)
S_StartSound(NULL, sfx_3db09);
if (newbuttons & MBT_C)
S_StartSound(NULL, sfx_s1be);
if (newbuttons & MBT_X)
S_StartSound(NULL, sfx_s1a4);
if (newbuttons & MBT_Y)
S_StartSound(NULL, sfx_s3kcas);
if (newbuttons & MBT_Z)
S_StartSound(NULL, sfx_s3kc3s);
if (newbuttons & MBT_START)
S_StartSound(NULL, sfx_gshdc);
optionsmenu.trycontroller = 5*TICRATE;
}
else
{
optionsmenu.trycontroller--;
}
if (optionsmenu.trycontroller == 0)
{
// Reset controls to that of the current profile.
profile_t *cpr = PR_GetProfile(cv_currprofile.value);
if (cpr == NULL)
cpr = PR_GetProfile(0); // Creating a profile at boot, revert to guest profile
G_ApplyControlScheme(0, cpr->controls);
}
return true;
}
if (optionsmenu.bindtimer)
return true; // Eat all inputs there. We'll use a stupid hack in M_Responder instead.
//SetDeviceOnPress(); // Update device constantly so that we don't stay stuck with otpions saying a device is unavailable just because we're mapping multiple devices...
if (M_MenuExtraPressed(pid))
{
if (M_ClearCurrentControl())
S_StartSound(NULL, sfx_monch);
optionsmenu.bindben = 0;
optionsmenu.bindben_swallow = M_OPTIONS_BINDBEN_QUICK;
M_SetMenuDelay(pid);
return true;
}
else if (M_MenuBackPressed(pid))
{
M_ProfileControlsConfirm(0);
M_SetMenuDelay(pid);
return true;
}
if (menucmd[pid].dpad_ud)
{
if (optionsmenu.bindben_swallow)
{
// Control would be cleared, but we're
// interrupting the animation so clear it
// immediately.
M_ClearCurrentControl();
}
optionsmenu.bindben = 0;
optionsmenu.bindben_swallow = 0;
}
return false;
}
void M_ProfileSetControl(INT32 ch)
{
(void) ch;
optionsmenu.bindtimer = TICRATE*5;
memset(optionsmenu.bindinputs, 0, sizeof optionsmenu.bindinputs);
G_ResetAllDeviceGameKeyDown();
}
static void M_ProfileDefaultControlsResponse(INT32 ch)
{
if (ch == MA_YES)
{
memcpy(&optionsmenu.tempcontrols, gamecontroldefault, sizeof optionsmenu.tempcontrols);
S_StartSound(NULL, sfx_s24f);
}
}
void M_ProfileDefaultControls(INT32 ch)
{
(void)ch;
M_StartMessage(
"Profiles",
"Reset all controls to the default mappings?",
&M_ProfileDefaultControlsResponse,
MM_YESNO,
NULL,
NULL
);
}
static void M_ProfileClearControlsResponse(INT32 ch)
{
if (ch == MA_YES)
{
memset(&optionsmenu.tempcontrols, 0, sizeof optionsmenu.tempcontrols);
S_StartSound(NULL, sfx_s3k66);
}
}
void M_ProfileClearControls(INT32 ch)
{
(void)ch;
M_StartMessage(
"Profiles",
"Clear all control bindings?",
&M_ProfileClearControlsResponse,
MM_YESNO,
NULL,
NULL
);
}
// Map the event to the profile.
#define KEYHOLDFOR 1
void M_MapProfileControl(event_t *ev)
{
if (ev->type == ev_keydown && ev->data2) // ignore repeating keys
return;
if (optionsmenu.bindtimer > TICRATE*5 - 9) // grace period after entering the bind dialog
return;
INT32 *DeviceGameKeyDownArray = G_GetDeviceGameKeyDownArray(ev->device);
if (!DeviceGameKeyDownArray)
return;
// Find every held button.
boolean noinput = true;
for (INT32 c = 1; c < NUMINPUTS; ++c)
{
if (DeviceGameKeyDownArray[c] < 3*JOYAXISRANGE/4)
continue;
noinput = false;
for (UINT8 i = 0; i < MAXINPUTMAPPING; ++i)
{
// If this key is already bound, don't bind it again.
if (optionsmenu.bindinputs[i] == c)
break;
// Find the first available slot.
if (!optionsmenu.bindinputs[i])
{
optionsmenu.bindinputs[i] = c;
break;
}
}
}
if (noinput)
{
{
// You can hold a button before entering this
// dialog, then buffer a keyup without pressing
// anything else. If this happens, don't wipe the
// binds, just ignore it.
const UINT8 zero[sizeof optionsmenu.bindinputs] = {0};
if (!memcmp(zero, optionsmenu.bindinputs, sizeof zero))
return;
}
INT32 controln = currentMenu->menuitems[itemOn].mvar1;
memcpy(&optionsmenu.tempcontrols[controln], optionsmenu.bindinputs, sizeof optionsmenu.bindinputs);
optionsmenu.bindtimer = 0;
// Set menu delay regardless of what we're doing to avoid stupid stuff.
M_SetMenuDelay(0);
}
else
optionsmenu.bindtimer = -1; // prevent skip countdown
}
#undef KEYHOLDFOR