RingRacers/src/g_build_ticcmd.cpp

484 lines
12 KiB
C++

// DR. ROBOTNIK'S RING RACERS
//-----------------------------------------------------------------------------
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 1998-2000 by DooM Legacy Team.
// Copyright (C) 1999-2020 by Sonic Team Junior.
// Copyright (C) 2023 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.
//-----------------------------------------------------------------------------
#include <algorithm>
#include <cmath>
#include "command.h"
#include "console.h"
#include "d_player.h"
#include "d_ticcmd.h"
#include "doomstat.h"
#include "doomtype.h"
#include "g_demo.h"
#include "g_game.h"
#include "g_input.h"
#include "g_state.h"
#include "g_party.h"
#include "hu_stuff.h"
#include "i_joy.h"
#include "i_system.h"
#include "k_bot.h"
#include "k_director.h"
#include "k_kart.h"
#include "k_menu.h"
#include "lua_hook.h"
#include "m_cheat.h"
#include "m_fixed.h"
#include "p_local.h"
#include "p_mobj.h"
#include "p_tick.h"
#include "tables.h"
#include "m_random.h" // monkey input
extern "C" consvar_t cv_1pswap;
namespace
{
// Take a magnitude of two axes, and adjust it to take out the deadzone
// Will return a value between 0 and JOYAXISRANGE
INT32 G_BasicDeadZoneCalculation(INT32 magnitude, fixed_t deadZone)
{
const INT32 jdeadzone = (JOYAXISRANGE * deadZone) / FRACUNIT;
INT32 adjustedMagnitude = std::abs(magnitude);
if (jdeadzone >= JOYAXISRANGE && adjustedMagnitude >= JOYAXISRANGE) // If the deadzone and magnitude are both 100%...
{
return JOYAXISRANGE; // ...return 100% input directly, to avoid dividing by 0
}
if (adjustedMagnitude <= jdeadzone)
{
return 0; // Magnitude is within deadzone, so do nothing
}
// Calculate how much the magnitude exceeds the deadzone
adjustedMagnitude = std::min(adjustedMagnitude, JOYAXISRANGE) - jdeadzone;
return (adjustedMagnitude * JOYAXISRANGE) / (JOYAXISRANGE - jdeadzone);
}
class TiccmdBuilder
{
struct JoyStickVector2
{
INT32 xaxis;
INT32 yaxis;
};
ticcmd_t* cmd;
INT32 realtics;
UINT8 ssplayer;
UINT8 viewnum = G_PartyPosition(g_localplayers[forplayer()]);
UINT8 pid = swap_ssplayer() - 1;
JoyStickVector2 joystickvector;
UINT8 forplayer() const { return ssplayer - 1; }
player_t* player() const { return &players[g_localplayers[forplayer()]]; }
bool freecam() const { return camera[forplayer()].freecam; }
UINT8 swap_ssplayer() const
{
if (ssplayer == cv_1pswap.value)
{
return 1;
}
else if (ssplayer == 1)
{
return cv_1pswap.value;
}
else
{
return ssplayer;
}
}
// Get the actual sensible radial value for a joystick axis when accounting for a deadzone
void handle_axis_deadzone()
{
INT32 gamepadStyle = Joystick[pid].bGamepadStyle;
fixed_t deadZone = cv_deadzone[pid].value;
// When gamepadstyle is "true" the values are just -1, 0, or 1. This is done in the interface code.
if (gamepadStyle)
{
return;
}
// Get the total magnitude of the 2 axes
INT32 magnitude = std::sqrt(static_cast<double>(
(joystickvector.xaxis * joystickvector.xaxis) + (joystickvector.yaxis * joystickvector.yaxis)
));
// Get the normalised xy values from the magnitude
INT32 normalisedXAxis = (joystickvector.xaxis * magnitude) / JOYAXISRANGE;
INT32 normalisedYAxis = (joystickvector.yaxis * magnitude) / JOYAXISRANGE;
// Apply the deadzone to the magnitude to give a correct value between 0 and JOYAXISRANGE
INT32 normalisedMagnitude = G_BasicDeadZoneCalculation(magnitude, deadZone);
// Apply the deadzone to the xy axes
joystickvector.xaxis = (normalisedXAxis * normalisedMagnitude) / JOYAXISRANGE;
joystickvector.yaxis = (normalisedYAxis * normalisedMagnitude) / JOYAXISRANGE;
// Cap the values so they don't go above the correct maximum
joystickvector.xaxis = std::min(joystickvector.xaxis, JOYAXISRANGE);
joystickvector.xaxis = std::max(joystickvector.xaxis, -JOYAXISRANGE);
joystickvector.yaxis = std::min(joystickvector.yaxis, JOYAXISRANGE);
joystickvector.yaxis = std::max(joystickvector.yaxis, -JOYAXISRANGE);
}
void hook()
{
/*
Lua: Allow this hook to overwrite ticcmd.
We check if we're actually in a level because for some reason this Hook would run in menus and on the titlescreen otherwise.
Be aware that within this hook, nothing but this player's cmd can be edited (otherwise we'd run in some pretty bad synching problems since this is clientsided, or something)
Possible usages for this are:
-Forcing the player to perform an action, which could otherwise require terrible, terrible hacking to replicate.
-Preventing the player to perform an action, which would ALSO require some weirdo hacks.
-Making some galaxy brain autopilot Lua if you're a masochist
-Making a Mario Kart 8 Deluxe tier baby mode that steers you away from walls and whatnot. You know what, do what you want!
*/
if (!addedtogame || gamestate != GS_LEVEL)
{
return;
}
LUA_HookTiccmd(player(), cmd, HOOK(PlayerCmd));
auto clamp = [](auto val, int range) { return std::clamp(static_cast<int>(val), -(range), range); };
cmd->forwardmove = clamp(cmd->forwardmove, MAXPLMOVE);
cmd->turning = clamp(cmd->turning, KART_FULLTURN);
cmd->throwdir = clamp(cmd->throwdir, 1);
// Send leveltime when this tic was generated to the server for control lag calculations.
// Only do this when in a level. Also do this after the hook, so that it can't overwrite this.
cmd->latency = (leveltime & TICCMD_LATENCYMASK);
}
// Turning was removed from G_BuildTiccmd to prevent easy client hacking.
// This brings back the camera prediction that was lost.
void angle_prediction()
{
// Chasecam stops in these situations, so local cam should stop too.
// Otherwise it'll jerk when it resumes.
if (player()->playerstate == PST_DEAD)
{
return;
}
if (player()->mo != NULL && !P_MobjWasRemoved(player()->mo) && player()->mo->hitlag > 0)
{
return;
}
angle_t angleChange = 0;
while (realtics > 0)
{
INT32& steering = localsteering[forplayer()];
steering = K_UpdateSteeringValue(steering, cmd->turning);
angleChange = K_GetKartTurnValue(player(), steering) << TICCMD_REDUCE;
realtics--;
}
#if 0
// Left here in case it needs unsealing later. This tried to replicate an old localcam function, but this behavior was unpopular in tests.
//if (player()->pflags & PF_DRIFTEND)
{
localangle[forplayer()] = player()->mo->angle;
}
else
#endif
{
int p = g_localplayers[forplayer()];
for (int i = 0; i <= r_splitscreen; ++i)
{
if (displayplayers[i] == p)
{
localangle[i] += angleChange;
}
}
}
}
bool typing_input()
{
if (!menuactive && !chat_on && !CON_Ready())
{
return false;
}
cmd->flags |= TICCMD_TYPING;
if (hu_keystrokes)
{
cmd->flags |= TICCMD_KEYSTROKE;
}
return true;
}
void toggle_freecam_input()
{
if (M_MenuButtonPressed(pid, MBT_C))
{
P_ToggleDemoCamera(forplayer());
}
}
bool director_input()
{
if (freecam() || !K_DirectorIsAvailable(viewnum))
{
return false;
}
if (M_MenuButtonPressed(pid, MBT_A))
{
G_AdjustView(ssplayer, 1, true);
K_ToggleDirector(forplayer(), false);
}
if (M_MenuButtonPressed(pid, MBT_X))
{
G_AdjustView(ssplayer, -1, true);
K_ToggleDirector(forplayer(), false);
}
if (player()->spectator == true)
{
// duplication of fire
if (G_PlayerInputDown(pid, gc_item, 0))
{
cmd->buttons |= BT_ATTACK;
}
if (M_MenuButtonPressed(pid, MBT_R))
{
K_ToggleDirector(forplayer(), true);
}
}
toggle_freecam_input();
return true;
}
bool spectator_analog_input()
{
if (!player()->spectator && !objectplacing && !freecam())
{
return false;
}
if (G_PlayerInputDown(pid, gc_accel, 0))
{
cmd->buttons |= BT_ACCELERATE;
}
if (G_PlayerInputDown(pid, gc_brake, 0))
{
cmd->buttons |= BT_BRAKE;
}
if (G_PlayerInputDown(pid, gc_lookback, 0))
{
cmd->aiming -= (joystickvector.yaxis * KART_FULLTURN) / JOYAXISRANGE;
}
else
{
if (joystickvector.yaxis < 0)
{
cmd->forwardmove += MAXPLMOVE;
}
if (joystickvector.yaxis > 0)
{
cmd->forwardmove -= MAXPLMOVE;
}
}
return true;
}
void kart_analog_input()
{
// forward with key or button // SRB2kart - we use an accel/brake instead of forward/backward.
INT32 value = G_PlayerInputAnalog(pid, gc_accel, 0);
if (value != 0)
{
cmd->buttons |= BT_ACCELERATE;
cmd->forwardmove += ((value * MAXPLMOVE) / JOYAXISRANGE);
}
value = G_PlayerInputAnalog(pid, gc_brake, 0);
if (value != 0)
{
cmd->buttons |= BT_BRAKE;
cmd->forwardmove -= ((value * MAXPLMOVE) / JOYAXISRANGE);
}
// But forward/backward IS used for aiming.
// throwdir > 0 throws forward, throwdir < 0 throws backward
// but we always use -1, 0 or 1 for consistency here.
// this allows the throw deadzone to be adjusted in the future without breaking demos
if (std::abs(joystickvector.yaxis) > JOYAXISRANGE / 2)
{
cmd->throwdir = -std::clamp(joystickvector.yaxis, -1, 1);
}
}
void analog_input()
{
joystickvector.xaxis = G_PlayerInputAnalog(pid, gc_right, 0) - G_PlayerInputAnalog(pid, gc_left, 0);
joystickvector.yaxis = 0;
handle_axis_deadzone();
// For kart, I've turned the aim axis into a digital axis because we only
// use it for aiming to throw items forward/backward and the vote screen
// This mean that the turn axis will still be gradient but up/down will be 0
// until the stick is pushed far enough
joystickvector.yaxis = G_PlayerInputAnalog(pid, gc_down, 0) - G_PlayerInputAnalog(pid, gc_up, 0);
if (encoremode)
{
joystickvector.xaxis = -joystickvector.xaxis;
}
if (joystickvector.xaxis != 0)
{
cmd->turning -= (joystickvector.xaxis * KART_FULLTURN) / JOYAXISRANGE;
}
if (spectator_analog_input())
{
return;
}
kart_analog_input();
// Digital users can input diagonal-back for shallow turns.
//
// There's probably some principled way of doing this in the gamepad handler itself,
// by only applying this filtering to inputs sourced from an axis. This is a little
// ugly with the current abstractions, though, and there's a fortunate trick here:
// if you can input full strength turns on both axes, either you're using a fucking
// square gate, or you're not on an analog device.
if (cv_litesteer[ssplayer - 1].value && joystickvector.yaxis >= JOYAXISRANGE && abs(cmd->turning) == KART_FULLTURN) // >= beacuse some analog devices can go past JOYAXISRANGE (?!)
cmd->turning /= 2;
}
void common_button_input()
{
auto map = [this](INT32 gamecontrol, UINT32 button)
{
if (G_PlayerInputDown(pid, gamecontrol, 0))
{
cmd->buttons |= button;
}
};
map(gc_drift, BT_DRIFT); // drift
map(gc_spindash, BT_SPINDASHMASK); // C
map(gc_item, BT_ATTACK); // fire
map(gc_lookback, BT_LOOKBACK); // rear view
map(gc_respawn, BT_RESPAWN | (freecam() ? 0 : BT_EBRAKEMASK)); // respawn
map(gc_vote, BT_VOTE); // mp general function button
// lua buttons a thru c
map(gc_luaa, BT_LUAA);
map(gc_luab, BT_LUAB);
map(gc_luac, BT_LUAC);
}
public:
explicit TiccmdBuilder(ticcmd_t* cmd_, INT32 realtics_, UINT8 ssplayer_) :
cmd(cmd_), realtics(realtics_), ssplayer(ssplayer_)
{
auto regular_input = [this]
{
analog_input();
common_button_input();
};
if (demo.playback || freecam() || player()->spectator)
{
// freecam is controllable even while paused
*cmd = {};
if (!typing_input() && !director_input())
{
regular_input();
if (freecam())
{
toggle_freecam_input();
}
}
return;
}
if (paused || P_AutoPause())
{
return;
}
*cmd = {}; // blank ticcmd
if (gamestate == GS_LEVEL && player()->playerstate == PST_REBORN)
{
return;
}
// A human player can turn into a bot at the end of
// a race, so the director controls have higher
// priority.
bool overlay = typing_input() || director_input();
if (K_PlayerUsesBotMovement(player()))
{
// Bot ticcmd is generated by K_BuildBotTiccmd
return;
}
if (!overlay)
{
regular_input();
}
cmd->angle = localangle[viewnum] >> TICCMD_REDUCE;
hook();
angle_prediction();
}
};
}; // namespace
void G_BuildTiccmd(ticcmd_t *cmd, INT32 realtics, UINT8 ssplayer)
{
TiccmdBuilder(cmd, realtics, ssplayer);
}