ts/src/shared/input.qc

787 lines
21 KiB
Plaintext

/*
* Copyright (c) 2016-2020 Marco Hladik <marco@icculus.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
void processInputs(void);
#ifdef CLIENT
void PreSpawn_Input(void);
// externs
void HUD_DrawWeaponSelect_Trigger(void);
BOOL TS_HUD_CloseWeaponSelect(BOOL);
#endif
#if OTHER_PREDICTION_TEST == 1
float custom_input_buttons;
float ary_custom_input_buttons[512];
BOOL ary_custom_input_sentYet[512];
void Custom_Game_Input(void);
#endif
// WARNING! This is only called by PMove_Run, which is only called
// for spawned players. This is not called for players in spectator,
// so if script to check for clicking while in spectator with no menu
// to spawn is here, prepare to be disappointed.
// Also, this is called by pmove.qc (Nuclide), and it trusts "self" regardless
// of being client or serverside, so going to here too.
// ALSO, no need for "is spectator" checks here. Only being a player allows
// pmove to be called to begin with.
void
Game_Input(void)
{
player pl = (player)self;
//
// TODO: in the CSQC_Input_Frame event might be better when supported
// (but still do a 'return' on not being spawned here regardless)
//
if(pl.iState != PLAYER_STATE::SPAWNED){
// not ingame (fake spectator)? Do another check instead: spawning.
#ifdef CLIENT
PreSpawn_Input();
#endif
// client or server, don't pay attention to the rest of this file if not spawned
return;
}
#ifdef CLIENT
else{
// not spawned? Some notes on possible things to involve, all dummied for now
// This was removed? Legacy VGUI
// If we are inside a UI, don't let the client do stuff outside
// (looks like this is no longer necessary)
/*
if (pl.iState != PLAYER_STATE::SPAWNED && pSeatLocal->m_flUI_Display != UI_SCREEN::NONE){
pSeat->m_flInputBlockTime = time + 0.2;
}
*/
// No need to check for calling TS_HUD_DrawWeaponSelect_CheckClick or
// HUD_DrawWeaponSelect_Trigger here, Nuclide calls the latter which works
// fine.
}// pi.iState checks
#endif
// Must be ingame to reach beyond here
///////////////////////////////////////////////////////////
// Anything special with my own vars for networking to work right here?
#ifdef CLIENT
pl.clientInputUpdate();
#endif
// This method, Game_Input, is called by Nuclide's pmove.qc in the same place
// w_attack_next is adjusted by the current frame time.
pl.updateTimers();
#ifdef SERVER
CGameRules rules = (CGameRules)g_grMode;
//TAGGG - why leave this up to any player? If all are in spectator this leaves the game stuck
if (rules.m_iIntermission) {
rules.IntermissionEnd();
return;
}
if (input_buttons & INPUT_BUTTON5)
Player_UseDown();
else
Player_UseUp();
self.impulse = 0;
#endif
/*
// FreeHL's way
if (input_buttons & INPUT_BUTTON0)
Weapons_Primary();
else if (input_buttons & INPUT_BUTTON4)
Weapons_Reload();
else if (input_buttons & INPUT_BUTTON3)
Weapons_Secondary();
//else
pl.callWeaponThink(); //Weapons_Release();
// Added portion to that.
if(input_buttons & INPUT_BUTTON0){
pl.gflags |= GF_SEMI_TOGGLED;
}else{
// held down the previous frame, but not now? That's a release.
if(pl.gflags & GF_SEMI_TOGGLED){
TS_Weapon_PrimaryAttackRelease(pl);
pl.gflags &= ~GF_SEMI_TOGGLED;
}
}
if(input_buttons & INPUT_BUTTON3){
pl.gflags |= GF_SEMI_SECONDARY_TOGGLED;
}else{
if(pl.gflags & GF_SEMI_SECONDARY_TOGGLED){
TS_Weapon_SecondaryAttackRelease(pl);
pl.gflags &= ~GF_SEMI_SECONDARY_TOGGLED;
}
}
// it's this way or the much more modified way further down
return;
*/
// TS way, weapon thinks happen alongside checking inputs
pl.callWeaponThink();
// HACK: If, this frame, w_attack_next was 0, for most weapons to allow firing,
// make that a signal to reset inputPrimaryTapFrameCount.
// This way, only the first frame to set w_attack_next benefits from inputPrimaryTapFrameCount.
// Running a weapon's _primary several times for the same tap is not wanted, this is only
// to give some tolerance to ease some slight frame desyncs between the client/server.
BOOL wasPassingFrame = (pl.w_attack_next <= 0);
#if INPUT_TAP_DETECT_CHOICE == 1
processInputs();
// !!!
// Should SEMI flags be handled here for anytime an input has been pressed
// (even if not involved due to the other being picked, like primary fire
// taking precedence when both are pressed so secondary is registered as
// held down the next frame, even when primary is let go but sec is not),
// or only on PRIMARY / SECONDARY being handled? Decisions decisions
if(input_buttons & INPUT_BUTTON0){
//pl.gflags |= GF_SEMI_TOGGLED;
}else{
// held down the previous frame, but not now? That's a release.
if(pl.gflags & GF_SEMI_TOGGLED){
TS_Weapon_PrimaryAttackRelease(pl);
pl.gflags &= ~GF_SEMI_TOGGLED;
}
}
if(input_buttons & INPUT_BUTTON3){
//pl.gflags |= GF_SEMI_SECONDARY_TOGGLED;
}else{
if(pl.gflags & GF_SEMI_SECONDARY_TOGGLED){
TS_Weapon_SecondaryAttackRelease(pl);
pl.gflags &= ~GF_SEMI_SECONDARY_TOGGLED;
}
}
#else
//////////////////////////////////////////////
//INPUT_TAP_DETECT_CHOICE == 2
if (input_buttons & INPUT_BUTTON0){
// Fresh touch? Set inputPrimaryTapFrameCount.
// The client gets an extra frame to count. This makes an issue where giving a semi input
// at a fringe time (like the immediate end of a weapon draw, reload, or ironsight-change
// anim) causes the anim to appear to repeat.
// This is because the client receives the semi-tap at a time when w_attack_next isn't
// ready, yet the complementary server frame passes. Typically, the very next client frame
// passes the w_attack_next check, but, being another frame while held-down, this is not
// a fresh semi-tap and so it does not count.
// In a primary attack method, if everything else goes right, it calls the view animation
// method, which does something a bit different client and serverside.
// Both set w_attack_next, but only clientside changes the sequence played.
// So only serverside being called resets the anim to re-play the previously picked sequence
// (becuase w_attack_next is networked, so it reaches the client from there anyway).
// So, giving the client an extra frame to count a semi-press one frame late (so long as the
// first does not pass w_attack_next) seems to make the issue much less likely in the very least.
// It may also suffice to only set the GF_SEMI_TOGGLED flag on running primary (that is, only on
// passing anything else like w_attack_next and clip checks, not just unconditionally on all frames
// like it is now). But this means holding down fire in an obviously non-fireable state
// (draw/reload/etc) lets it fire once the moment that ends. Not terrible but up to taste.
// And yes, this has been tested in FreeHL.
// To be far more likely to happen, enable semi-press detection on the glock and set the player's
// w_attack_next to 31.0f/30.0f in the _draw method. Why is that more likely to make the issue
// occur? Absolutely mystifying, being a clean 0.5 or 1.0 seconds makes it much less likely to
// occur. I can only thing some oddness in cumulative subtractions in bringing the w_attack_next
// to 0 getting very close to 0 without passing it, so much so that a tiny variance like client/server
// differences, even on the same machine, are enough to make one ready in the same perceived frame
// but the other not. Server or client-exclusive lag may also make the issue more likely even in
// clean delays for FreeHL, particularly spamming printouts.
if(!(pl.gflags & GF_SEMI_TOGGLED)){
#ifdef CLIENT
pl.inputPrimaryTapFrameCount = 2;
//pl.inputPrimaryTapFrameCount = 1;
#else
pl.inputPrimaryTapFrameCount = 1;
#endif
}
pl.gflags |= GF_SEMI_TOGGLED;
pl.gflags_net |= GF_SEMI_TOGGLED;
}else{
// Not pressed? See if this is a release
if(pl.gflags & GF_SEMI_TOGGLED){
// Held the previous frame? Mark this
pl.inputPrimaryReleasedQueue = TRUE;
}
// mark as not held down (for the next time held down to be a fresh press)
pl.gflags &= ~GF_SEMI_TOGGLED;
pl.gflags_net &= ~GF_SEMI_TOGGLED;
}
// Same for secondary.
if (input_buttons & INPUT_BUTTON3){
if(!(pl.gflags & GF_SEMI_SECONDARY_TOGGLED)){
#ifdef CLIENT
pl.inputSecondaryTapFrameCount = 2;
#else
pl.inputSecondaryTapFrameCount = 1;
#endif
}
pl.gflags |= GF_SEMI_SECONDARY_TOGGLED;
pl.gflags_net |= GF_SEMI_SECONDARY_TOGGLED;
}else{
// Not pressed? See if this is a release
if(pl.gflags & GF_SEMI_SECONDARY_TOGGLED){
// Held the previous frame? Mark this
pl.inputSecondaryReleasedQueue = TRUE;
}
// mark as not held down (for the next time held down to be a fresh press)
pl.gflags &= ~GF_SEMI_SECONDARY_TOGGLED;
pl.gflags_net &= ~GF_SEMI_SECONDARY_TOGGLED;
}
// This way, what's below the script in this method returning early doesn't skip what comes after.
processInputs();
if(wasPassingFrame){
pl.inputPrimaryTapFrameCount = 0;
pl.inputSecondaryTapFrameCount = 0;
}
if(pl.inputPrimaryTapFrameCount > 0){
pl.inputPrimaryTapFrameCount--;
}
if(pl.inputSecondaryTapFrameCount > 0){
pl.inputSecondaryTapFrameCount--;
}
// End of the time to count a fresh tap, and there was a recent release? Act on it.
// Unsure if the FrameCount check should be done at all here.
if(pl.inputPrimaryReleasedQueue && pl.inputPrimaryTapFrameCount == 0){
pl.inputPrimaryReleasedQueue = FALSE;
// TODO - remove 2nd parameter from that!
TS_Weapon_PrimaryAttackRelease(pl);
}
if(pl.inputSecondaryReleasedQueue && pl.inputSecondaryTapFrameCount == 0){
pl.inputSecondaryReleasedQueue = FALSE;
// TODO - remove 2nd parameter from that!
TS_Weapon_SecondaryAttackRelease(pl);
}
#endif// INPUT_TAP_DETECT_CHOICE
//pl.callWeaponThink();
#if OTHER_PREDICTION_TEST == 2
/*
if(input_impulse == 115){
TS_playerUseItems();
}
*/
if(input_impulse == 115){
if( !(pl.gflags & GF_UNUSED4)){
TS_playerUseItems();
}
pl.gflags |= GF_UNUSED4;
}else{
pl.gflags &= ~GF_UNUSED4;
}
#endif
if(input_impulse == 116){
if( !(pl.gflags & GF_UNUSED5)){
TS_playerChangeFiremode();
}
pl.gflags |= GF_UNUSED5;
}else{
pl.gflags &= ~GF_UNUSED5;
}
}//Game_Input
void processInputs(void){
player pl = (player)self;
if (input_buttons & INPUT_BUTTON0){
//printfline("!!! PRIMA !!! (%d %d)", ((input_buttons & INPUT_BUTTON0)!=0), ((input_buttons & INPUT_BUTTON3)!=0));
Weapons_Primary();
pl.gflags |= GF_SEMI_TOGGLED;
return;
}
if (input_buttons & INPUT_BUTTON3){
//printfline("!!! SECO !!! (%d %d)", ((input_buttons & INPUT_BUTTON0)!=0), ((input_buttons & INPUT_BUTTON3)!=0));
Weapons_Secondary();
// TEST!
//if(!(pl.gflags & GF_SEMI_SECONDARY_TOGGLED)){
// TS_playerUseItems();
//}
pl.gflags |= GF_SEMI_SECONDARY_TOGGLED;
return;
}
// why did the reload check come before Secondary-fire checks before??
if (input_buttons & INPUT_BUTTON4){
Weapons_Reload();
// Picked reload? Turn the flags for both fire types off
//pl.gflags &= ~(GF_SEMI_TOGGLED | GF_SEMI_SECONDARY_TOGGLED);
return;
}
// Below's changes to pl.gFlags for GF_SEMI_TOGGLED (Weapons_Release) and SECONDARY
// might be a little redundant at this point, but that is ok.
// reached here?
Weapons_Release();
// No need for this now
/*
// These SEMI flag removals only happen if this area is reached
// (note the 'return' statements above with button presses)
// Nuclide already does that first one (only if Weapons_Release is called!)
//pl.gflags &= ~GF_SEMI_TOGGLED;
pl.gflags &= ~GF_SEMI_SECONDARY_TOGGLED;
*/
}//processInputs
#ifdef CLIENT
// called when not ingame for sending a spawn request.
void PreSpawn_Input(void){
player pl = (player)self;
if(
pSeatLocal->m_flUI_Display == UI_SCREEN::NONE &&
(input_buttons & INPUT_BUTTON0) &&
!(pl.gflags & GF_SEMI_TOGGLED) &&
pSeatLocal->m_flBlockSpawnInputTime <= time
){
sendevent( "GamePlayerSpawn", "");
}
/*
if(
((input_buttons & INPUT_BUTTON0) && pSeatLocal->m_flBlockSpawnInputTime > time) ||
pSeat->m_flInputBlockTime > time
)
{
pSeatLocal->m_flBlockSpawnInputTime = time + 0.20f;
pl.gflags |= GF_SEMI_TOGGLED;
return;
}
*/
// since the rest of this method is about to be skipped.
if(input_buttons & INPUT_BUTTON0){
pl.gflags |= GF_SEMI_TOGGLED;
}else{
pl.gflags &= ~GF_SEMI_TOGGLED;
}
}//PreSpawn_Input
#ifdef CLIENT
// redirects from cmd.qc to be less of a mess over there.
void Input_useItems_press(void){
player pl = (player)pSeat->m_ePlayer;
#if OTHER_PREDICTION_TEST == 0
if( !(pl.gflags & GF_UNUSED3)){
TS_playerUseItems();
}
pl.gflags |= GF_UNUSED3;
#elif OTHER_PREDICTION_TEST == 1
pSeatLocal->m_bUseItems = TRUE;
#elif OTHER_PREDICTION_TEST == 2
if( !(pl.gflags & GF_UNUSED3)){
canPlaySwitchSound = TRUE;
}else{
pl.gflags |= GF_UNUSED3;
}
pSeatLocal->m_bUseItems = TRUE;
#endif
}//Input_useItems_press
void Input_useItems_release(void){
player pl = (player)pSeat->m_ePlayer;
#if OTHER_PREDICTION_TEST == 0
pl.gflags &= ~GF_UNUSED3;
#elif OTHER_PREDICTION_TEST == 1
pSeatLocal->m_bUseItems = FALSE;
#elif OTHER_PREDICTION_TEST == 2
pSeatLocal->m_bUseItems = FALSE;
pl.gflags &= ~GF_UNUSED3;
#endif
}//Input_useItems_release
#endif
void Input_firemode_press(void){
pSeatLocal->m_bChangeFiremode = TRUE;
}
void Input_firemode_release(void){
pSeatLocal->m_bChangeFiremode = FALSE;
}
// Not yet called by Nuclide! A series of notes for now
// (move to clientside files then)
//TODO: if spawning from seemingly the same click as "use new config" is still a problem, this is
// definitely the place to try something
void ClientGame_Input_Frame(void){
// This is trying to close weaponselect on detecting a right-click, but it
// has issues.
// Idea is, this needs to *absorb* the right-click, and stop it from reaching
// the rest of the client and server to work with weapons, like a change-ironsight
// order.
// This might stop the client weapon logic from seeing the right click, but not
// the server.
// It appears there is no way to stop that without the FTE built-in event method
// CSQC_Input_Frame (defined by Nuclide) letting the gamemod block right-click
// inputs per some condition, like weapon-select being up.
// TODO - changing what m_flInputBlockTime affects might be a good idea,
// affecting other inputs like crouch is unnecessary, that causes the player to
// uncrouch if changing weapons while crouched.
player pl = (player)pSeat->m_ePlayer;
if(pl == NULL)return; // ???
/*
// does... funky things, not cumulative, only an offset forced this frame.
// Don't do that.
input_angles[0] += 36;
input_angles[1] += 36;
input_angles[2] += 36;
*/
if(pl.iState != PLAYER_STATE::SPAWNED && pSeatLocal->m_flUI_Display != UI_SCREEN::NONE){
// If buying, don't allow any movement inputs.
// The above is a similar check as "g_vguiWidgetCount > 0", but the buymenu does not
// use VGUI.
input_movevalues = [0,0,0];
// does not stop rotation from left/right arrows, not that this was very important.
//input_angles = [0,0,0];
}
if(pSeat->m_iHUDWeaponSelected && pSeat->m_iInputAttack2){ //input_buttons & INPUT_BUTTON3
if(TS_HUD_CloseWeaponSelect(TRUE)){
// might not even need this much, but verify.
// might change going to weapons that have an immediate secondary fire
//pSeat->m_flInputBlockTime = time + 0.2;
input_impulse = 0;
//input_buttons = 0; no need for the whole buttons reset
input_buttons &= ~INPUT_BUTTON3;
pSeat->m_iInputAttack2 = FALSE;
}else{
//pSeat->m_iInputAttack2 = TRUE;
}
}
// !!!
// An override for this to go in CSQC_Input_Frame would be very nice I think,
// unless there is some other way that I'm missing.
if (pSeatLocal->m_iInputSpeed == TRUE) {
input_buttons |= INPUT_BUTTON7;
//self.flags |= FL_SNEAK;
}else{
//self.flags &= ~FL_SNEAK;
}
//printfline("input_buttons: %d", (INPUT_BUTTON7 & input_buttons) );
#if OTHER_PREDICTION_TEST == 2
if(pSeatLocal->m_bUseItems){
input_impulse = 115;
}
/*
if(pSeatLocal->m_bUseItems){
if( !(pl.gflags & GF_UNUSED4)){
input_impulse = 115;
}
pl.gflags |= GF_UNUSED4;
}else{
pl.gflags &= ~GF_UNUSED4;
}
*/
#endif
if(pSeatLocal->m_bChangeFiremode){
input_impulse = 116;
}
}// ClientGame_InputFrame
#endif // CLIENT
//TAGGG - most of the alternate prediction demo.
// Replicates what's seen in Nuclide's src/predict.qc to make
// the toggle-options feature predicted without involving FTE's
// input_buttons.
//////////////////////////////////////////////////////////////////////////////////
#if OTHER_PREDICTION_TEST == 1
// Shared
void Custom_Game_Input(void){
player pl = (player)self;
if(custom_input_buttons == TRUE){
if( !(pl.gflags & GF_UNUSED3)){
//printfline("---CLICK---");
TS_playerUseItems();
}
pl.gflags |= GF_UNUSED3;
}else{
pl.gflags &= ~GF_UNUSED3;
}
}//Custom_Game_Input
#ifdef CLIENT
void Custom_ClientGame_Input_Frame(void){
if(pSeatLocal->m_bUseItems){
//pSeatLocal->m_bUseItems = FALSE;
//input_buttons |= INPUT_BUTTON6;
ary_custom_input_buttons[custom_clientcommandframe % 512] = TRUE;
}
if(!ary_custom_input_sentYet[custom_clientcommandframe % 512]){
sendevent("GSVRCC", "ff", custom_clientcommandframe, ary_custom_input_buttons[custom_clientcommandframe % 512]);
ary_custom_input_sentYet[custom_clientcommandframe % 512] = TRUE;
}
//printfline("CURRENT CLIENTCOMMAND FRAME: %d", custom_clientcommandframe);
// And refresh the entry in the queue somewhere before this point, so that
// loop-arounds don't cause issues from running into leftover/out-of-date
// memory being set but not being worth paying attention to.
float subtr = custom_clientcommandframe - 256;
if(subtr < 0){subtr += 512;}
float modulo = subtr % 512;
ary_custom_input_buttons[modulo] = 0;
ary_custom_input_sentYet[modulo] = FALSE;
}
void Custom_Predict_EntityUpdate(player pl){
if (pl.entnum == player_localentnum) {
/* run the player physics from the last approved servercommandframe to the current one */
for (int i = pl.custom_sequence+1; i <= custom_servercommandframe; i++) {
/* ...maybe the input state is too old? */
//if (!getinputstate(i)) {
// break;
//}
custom_input_sequence = i;
//pl.Physics_Run();
custom_input_buttons = ary_custom_input_buttons[custom_input_sequence % 512];
Custom_Game_Input();
}
}
pl.custom_sequence = custom_servercommandframe;
}
void Custom_Predict_PlayerPreFrame(player pl){
//printfline("---PREFRAME---");
for (int i = pl.custom_sequence + 1; i <= custom_clientcommandframe; i++) {
//float flSuccess = getinputstate(i);
//if (flSuccess == FALSE) {
// continue;
//}
if (i==custom_clientcommandframe){
//CSQC_Input_Frame();
Custom_ClientGame_Input_Frame();
}
/* don't do partial frames, aka incomplete input packets */
//if (input_timelength == 0) {
// break;
//}
/* this global is for our shared random number seed */
custom_input_sequence = i;
/* run our custom physics */
//pl.Physics_Run();
custom_input_buttons = ary_custom_input_buttons[custom_input_sequence % 512];
Custom_Game_Input();
}
custom_clientcommandframe++;
}
void Custom_Predict_PlayerPostFrame(player pl){
//printfline("---ROLLBACK---");
}
// called by server
void Custom_Prediction_Server_Callback(player pl){
float received_servercommandframe = readfloat();
custom_servercommandframe = received_servercommandframe;
if(pl != NULL){
Custom_Predict_EntityUpdate(pl);
}
if(pl != NULL){
int EXTRA = readint();
if(EXTRA > -1){
pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on = EXTRA;
//pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on_net = pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on;
}
}
}
#else
// SERVER
// short for GameServer_RunClientCommand.
void
CSEv_GSVRCC_ff(float arg_scf, float arg_ibc){
//TS_playerUseItems();
custom_servercommandframe = arg_scf;
custom_input_buttons = arg_ibc;
Custom_Game_Input();
// send a message back!
player pl = (player)self;
WriteByte( MSG_MULTICAST, SVC_CGAMEPACKET );
WriteByte( MSG_MULTICAST, EVENT_TS::CUSTOM_PREDICTION_CALLBACK );
WriteFloat( MSG_MULTICAST, custom_servercommandframe );
if(pl.inventoryEquippedIndex > -1){
WriteInt( MSG_MULTICAST, pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on );
}else{
WriteInt( MSG_MULTICAST, -1 );
}
msg_entity = pl;
multicast( [0,0,0], MULTICAST_ONE );
}
void Custom_EvaluateEntity(player pl){
// SAVE_STATE
//if(pl.inventoryEquippedIndex > -1){
// pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on_net = pl.ary_myWeapons[pl.inventoryEquippedIndex].iBitsUpgrade_on;
//}
}
#endif// CLIENT vs. SERVER
#endif// OTHER_PREDICTION_TEST
//////////////////////////////////////////////////////////////////////////////////