From 9c723da099ba4ccfa58cc1298a2abe64c65c28ff Mon Sep 17 00:00:00 2001 From: Robert Cochran Date: Tue, 27 Jan 2026 22:24:57 -0800 Subject: [PATCH] WIP: Initial parts of Steam Input API support --- bin/amd64/game_actions_967460.vdf | 99 +++++++++++ src/engine/cdpi.cpp | 6 + src/engine/engine.h | 6 + src/engine/main.cpp | 3 + src/game/controller.cpp | 269 ++++++++++++++++++++++++++++++ src/game/controller.h | 6 + src/game/game.cpp | 14 +- src/game/game.h | 6 + src/game/physics.cpp | 2 + 9 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 bin/amd64/game_actions_967460.vdf create mode 100644 src/game/controller.cpp create mode 100644 src/game/controller.h diff --git a/bin/amd64/game_actions_967460.vdf b/bin/amd64/game_actions_967460.vdf new file mode 100644 index 000000000..ea3416488 --- /dev/null +++ b/bin/amd64/game_actions_967460.vdf @@ -0,0 +1,99 @@ +"In Game Actions" +{ + "actions" + { + "InGameControls" + { + "title" "#Set_Ingame" + "StickPadGyro" + { + "move" + { + "title" "#Action_Move" + "input_mode" "joystick_move" + } + "camera" + { + "title" "#Action_Camera" + "input_mode" "absolute_mouse" + } + } + "Button" + { + "primary" "#Action_Primary" + "secondary" "#Action_Secondary" + "reload" "#Action_Reload" + "use" "#Action_Use" + "jump" "#Action_Jump" + "walk" "#Action_Walk" + "crouch" "#Action_Crouch" + "special" "#Action_Special" + "drop" "#Action_Drop" + "affinity" "#Action_Affinity" + "dash" "#Action_Dash" + + "next_weapon" "#Action_NextWeapon" + "previous_weapon" "#Action_PreviousWeapon" + "primary_weapon" "#Action_PrimaryWeapon" + "secondary_weapon" "#Action_SecondaryWeapon" + "wheel_select" "#Action_WheelSelect" + "change_loadout" "#Action_ChangeLoadout" + + "scoreboard" "#Action_Scoreboard" + "suicide" "#Action_Suicide" + + "recenter_camera" "#Action_RecenterCamera" + + } + } + "MenuControls" + { + "title" "#Set_Menu" + "StickPadGyro" + { + } + "AnalogTrigger" + { + } + "Button" + { + "menu_up" "#Menu_Up" + "menu_down" "#Menu_Down" + "menu_left" "#Menu_Left" + "menu_right" "#Menu_Right" + "menu_select" "#Menu_Select" + "menu_cancel" "#Menu_Cancel" + "pause_menu" "#Action_ReturnToGame" + } + } + } + "localization" + { + "english" + { + "Set_Ingame" "In-Game Controls" + "Set_Menu" "Menu Controls" + "Action_Move" "Move" + "Action_Camera" "Camera" + "Action_Primary" "Primary Fire" + "Action_Secondary" "Secondary Fire" + "Action_Reload" "Reload" + "Action_Use" "Use" + "Action_Jump" "Jump" + "Action_Walk" "Walk" + "Action_Crouch" "Crouch" + "Action_Special" "Special" + "Action_Drop" "Drop" + "Action_Affinity" "Affinity" + "Action_Dash" "Dash" + + "Action_RecenterCamera" "Recenter Camera" + "Menu_Up" "Up" + "Menu_Down" "Down" + "Menu_Left" "Left" + "Menu_Right" "Right" + "Menu_Select" "Select" + "Menu_Cancel" "Cancel" + } + } +} \ No newline at end of file diff --git a/src/engine/cdpi.cpp b/src/engine/cdpi.cpp index 8e0a3558a..76ac92d2a 100644 --- a/src/engine/cdpi.cpp +++ b/src/engine/cdpi.cpp @@ -1,6 +1,7 @@ // Content Delivery Platform Integrations #include "engine.h" +#include "controller.h" #include #if defined(USE_STEAM) #define HAS_STEAM 1 @@ -40,6 +41,7 @@ namespace cdpi ISteamUserStats *stats = NULL; ISteamClient *client = NULL, *sclient = NULL; ISteamGameServer *serv = NULL; + ISteamInput *input = NULL; HSteamPipe umpipe = 0, smpipe = 0; HSteamUser uupipe = 0, supipe = 0; HAuthTicket authticket = k_HAuthTicketInvalid; @@ -184,6 +186,10 @@ namespace cdpi if(!friends) { conoutf(colourred, "Failed to get Steam friends interface."); cleanup(SWCLIENT); return true; } stats = (ISteamUserStats *)SteamAPI_ISteamClient_GetISteamUserStats(client, uupipe, umpipe, STEAMUSERSTATS_INTERFACE_VERSION); if(!stats) { conoutf(colourred, "Failed to get Steam stats interface."); cleanup(SWCLIENT); return true; } + input = (ISteamInput *)SteamAPI_ISteamClient_GetISteamInput(client, uupipe, umpipe, STEAMINPUT_INTERFACE_VERSION); + if (!input) { conoutf(colourred, "Failed to get Steam Input interface."); cleanup(SWCLIENT); return true; } + input->Init(false); + controller::init_action_handles(); const char *name = SteamAPI_ISteamFriends_GetPersonaName(friends); if(name && *name) diff --git a/src/engine/engine.h b/src/engine/engine.h index c0c2d7da1..21dbaebf3 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -3,6 +3,9 @@ #include "version.h" #include "cube.h" +#if defined(USE_STEAM) +#include "steam_api_flat.h" +#endif #define LOG_FILE "log.txt" @@ -49,6 +52,9 @@ namespace cdpi namespace steam { extern char *steamusername, *steamuserid, *steamserverid; + #if defined(USE_STEAM) + extern ISteamInput *input; + #endif extern bool clientready(); extern bool clientauthticket(char *token, uint *tokenlen, ENetAddress *addr = NULL); diff --git a/src/engine/main.cpp b/src/engine/main.cpp index eff8bdc71..d6c0ccd3f 100644 --- a/src/engine/main.cpp +++ b/src/engine/main.cpp @@ -1,7 +1,9 @@ // main.cpp: initialisation & main loop #include "engine.h" +#include "controller.h" #include +#include #ifdef SDL_VIDEO_DRIVER_X11 #include "SDL_syswm.h" @@ -824,6 +826,7 @@ void checkinput() warping = false; if(grabinput && shouldwarp) resetcursor(true, false); } + controller::update_from_controller(); } void swapbuffers(bool overlay) diff --git a/src/game/controller.cpp b/src/game/controller.cpp new file mode 100644 index 000000000..cd4233b1a --- /dev/null +++ b/src/game/controller.cpp @@ -0,0 +1,269 @@ +#include "controller.h" +#include "game.h" +#include "engine.h" + +#if defined(USE_STEAM) +#include "steam_api_flat.h" +#endif + +#include + +#define DEF_ACTION_SET(x) InputActionSetHandle_t x##_handle = -1 +#define DEF_ANALOG_ACTION(x) InputAnalogActionHandle_t x##_handle = -1 +#define DEF_DIGITAL_ACTION(x) class digital_action_state x + +#define SET_ACTION_SET(x) x##_handle = cdpi::steam::input->GetActionSetHandle(#x) +#define SET_ANALOG_ACTION(x) x##_handle =cdpi::steam::input->GetAnalogActionHandle(#x) +#define SET_DIGITAL_ACTION(x) x.set_action_handle(cdpi::steam::input->GetDigitalActionHandle(#x)) + +namespace controller +{ +// If we are not using the SIAPI move action, then do not overwrite strafing +// data set by the regular keyboard bindings +bool lastmovementwaskeyboard = true; + +// This current controller implementation depends on Steam Input and is not +// available outside of Steam +#if defined(USE_STEAM) + +InputHandle_t *controllers = new InputHandle_t[STEAM_INPUT_MAX_COUNT]; + +class digital_action_state +{ + bool input_last_frame = false; + bool input_this_frame = false; + InputDigitalActionHandle_t handle = -1; + +public: + void set_action_handle(InputDigitalActionHandle_t handle) + { + this->handle = handle; + } + + bool get_digital_action_state() + { + InputDigitalActionData_t data = cdpi::steam::input->GetDigitalActionData(controllers[0], this->handle); + return data.bState; + } + + void update() + { + this->input_last_frame = this->input_this_frame; + this->input_this_frame = this->get_digital_action_state(); + } + + bool pressed() + { + return this->input_this_frame; + } + + bool released() + { + return !this->input_this_frame; + } + + bool just_pressed() + { + return this->input_this_frame && !this->input_last_frame; + } + + bool just_released() + { + return !this->input_this_frame && this->input_last_frame; + } +}; + +DEF_ACTION_SET(InGameControls); +DEF_ACTION_SET(MenuControls); + +DEF_ANALOG_ACTION(move); +DEF_ANALOG_ACTION(camera); + +DEF_DIGITAL_ACTION(primary); +DEF_DIGITAL_ACTION(secondary); +DEF_DIGITAL_ACTION(reload); +DEF_DIGITAL_ACTION(use); +DEF_DIGITAL_ACTION(jump); +DEF_DIGITAL_ACTION(walk); +DEF_DIGITAL_ACTION(crouch); +DEF_DIGITAL_ACTION(special); +DEF_DIGITAL_ACTION(drop); +DEF_DIGITAL_ACTION(affinity); +DEF_DIGITAL_ACTION(dash); + +DEF_DIGITAL_ACTION(next_weapon); +DEF_DIGITAL_ACTION(previous_weapon); +DEF_DIGITAL_ACTION(primary_weapon); +DEF_DIGITAL_ACTION(secondary_weapon); +DEF_DIGITAL_ACTION(wheel_select); +DEF_DIGITAL_ACTION(change_loadout); + +DEF_DIGITAL_ACTION(scoreboard); + +DEF_DIGITAL_ACTION(recenter_camera); + +bool get_digital_action_state(int siapi_digital_handle) +{ + InputDigitalActionData_t data = cdpi::steam::input->GetDigitalActionData(controllers[0], siapi_digital_handle); + return data.bState; +} + +void init_action_handles() +{ + SET_ACTION_SET(InGameControls); + SET_ACTION_SET(MenuControls); + + SET_ANALOG_ACTION(move); + SET_ANALOG_ACTION(camera); + + SET_DIGITAL_ACTION(primary); + SET_DIGITAL_ACTION(secondary); + SET_DIGITAL_ACTION(reload); + SET_DIGITAL_ACTION(use); + SET_DIGITAL_ACTION(jump); + SET_DIGITAL_ACTION(walk); + SET_DIGITAL_ACTION(crouch); + SET_DIGITAL_ACTION(special); + SET_DIGITAL_ACTION(drop); + SET_DIGITAL_ACTION(affinity); + //SET_DIGITAL_ACTION(dash); + //SET_DIGITAL_ACTION(next_weapon); + //SET_DIGITAL_ACTION(previous_weapon); + //SET_DIGITAL_ACTION(primary_weapon); + //SET_DIGITAL_ACTION(secondary_weapon); + //SET_DIGITAL_ACTION(wheel_select); + //SET_DIGITAL_ACTION(change_loadout); + SET_DIGITAL_ACTION(scoreboard); // showscores + + SET_DIGITAL_ACTION(recenter_camera); +} + +void handle_digital_action_ac(class digital_action_state *das, int ac) +{ + das->update(); + + if (das->just_pressed()) + physics::doaction(ac, true); + else if (das->just_released()) + physics::doaction(ac, false); +} + +void update_from_controller() +{ + // Steamworks ( https://partner.steamgames.com/doc/api/ISteamInput#RunFrame ) says that + // > Synchronize API state with the latest Steam Controller inputs + // > available. This is performed automatically by + // > SteamAPI_RunCallbacks, but for the absolute lowest possible + // > latency, you can call this directly before reading controller + // > state. + // which appears to be necessary here, otherwise we seem to drop some + // gamepad inputs + cdpi::steam::input->RunFrame(); + + int connected_count = cdpi::steam::input->GetConnectedControllers(controllers); + InputActionSetHandle_t current_set = hud::hasinput(true) ? MenuControls_handle : InGameControls_handle; + cdpi::steam::input->ActivateActionSet(STEAM_INPUT_HANDLE_ALL_CONTROLLERS, current_set); + InputAnalogActionData_t move_data = cdpi::steam::input->GetAnalogActionData( + controllers[0], + move_handle + ); + + //game::player1->move = move_data.y; + + if (move_data.y < -0.5f) + game::player1->move = -1; + else if (move_data.y > 0.5f) + game::player1->move = 1; + else if (!lastmovementwaskeyboard) + game::player1->move = 0; + + //game::player1->strafe = -move_data.x; + + if (move_data.x < -0.5f) + game::player1->strafe = 1; + else if (move_data.x > 0.5f) + game::player1->strafe = -1; + else if (!lastmovementwaskeyboard) + game::player1->strafe = 0; + + if (move_data.x != 0 && move_data.y != 0) + lastmovementwaskeyboard = false; + + // We have to read the camera delta every frame even if we don't intend + // on doing anything with it, otherwise it will 'build up', which is not + // what we want in the cases where we are going to deliberately ignore + // it. + InputAnalogActionData_t camera_delta = cdpi::steam::input->GetAnalogActionData( + controllers[0], + camera_handle + ); + + recenter_camera.update(); + if (recenter_camera.pressed()) { + game::player1->pitch = 0.0f; + } else { + // We deliberately *do not* respect the mouse sensitivity + // settings here. The Steamworks page 'getting started' page ( + // https://partner.steamgames.com/doc/features/steam_controller/getting_started_for_devs + // ) explicitly states that: + + // > You should either rely on the configurator to provide + // > sensitivity (ie you don't filter incoming Steam Input + // > data), or you should use a dedicated sensitivity option for + // > Steam Input that's distinct from the system mouse. + + // We opt to take Option A - make gamepad aim sensitivity + // entirely the responsibility of Steam Input. This reduces the + // amount of additional support code needed in-engine and also + // has the added benefit that the 'Dots per 360' setting for + // flick stick and RWS gyro configuration is always a fixed + // value in every configuration. (This value is 3600, for the + // record). We choose to make the universal base sensitivity + // the value exactly the same as the default sensitivity for + // mouse, so that Steam Input 100% sensitivity matches the + // game's default setting. + + game::player1->yaw += mousesens(camera_delta.x, 100.f, 10.f * game::zoomsens()); + game::player1->pitch -= mousesens(camera_delta.y, 100.f, 10.f * game::zoomsens()); + fixrange(game::player1->yaw, game::player1->pitch); + } + + // WIP: these things all work fine because I am not trying to call + // commands to make them go + handle_digital_action_ac(&primary, AC_PRIMARY); + handle_digital_action_ac(&secondary, AC_SECONDARY); + handle_digital_action_ac(&reload, AC_RELOAD); + handle_digital_action_ac(&use, AC_USE); + handle_digital_action_ac(&jump, AC_JUMP); + handle_digital_action_ac(&walk, AC_WALK); + handle_digital_action_ac(&crouch, AC_CROUCH); + handle_digital_action_ac(&special, AC_SPECIAL); + handle_digital_action_ac(&drop, AC_DROP); + handle_digital_action_ac(&affinity, AC_AFFINITY); + + // WIP: this thing does not work; I don't know how to call commands + // directly + scoreboard.update(); + tagval tv; + if (scoreboard.just_pressed()) { + printf("just pressed scoreboard\n"); + tv.setint(1); + execute(getident("showscores"), &tv, 1); + } else if (scoreboard.just_released()) { + printf("just released scoreboard\n"); + tv.setint(0); + execute(getident("showscores"), &tv, 1); + } +} +#else /* defined(USE_STEAM) */ +void init_action_handles() +{ + return; +} + +void update_from_controller() +{ + return; +} +#endif /* defined(USE_STEAM) */ +} diff --git a/src/game/controller.h b/src/game/controller.h new file mode 100644 index 000000000..f3bc9b901 --- /dev/null +++ b/src/game/controller.h @@ -0,0 +1,6 @@ +namespace controller +{ + extern bool lastmovementwaskeyboard; + extern void init_action_handles(); + extern void update_from_controller(); +} diff --git a/src/game/game.cpp b/src/game/game.cpp index 9e59bd977..fef9ca280 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -6,7 +6,7 @@ namespace game int nextmode = G_EDITING, nextmuts = 0, gamestate = G_S_WAITING, gamemode = G_EDITING, mutators = 0, maptime = 0, mapstart = 0, timeremaining = 0, timeelapsed = 0, timelast = 0, timewait = 0, timesync = 0, lastcamera = 0, lasttvcam = 0, lasttvchg = 0, lastzoom = 0, lastcamcn = -1; - bool zooming = false, inputmouse = false, inputview = false, inputmode = false, wantsloadoutmenu = false, hasspotlights = false; + bool zooming = false, inputmouse = false, inputview = false, inputmode = false, wantsloadoutmenu = false, hasspotlights = false, lastinputwaskeyboard = true; float swayfade = 0, swayspeed = 0, swaydist = 0, bobfade = 0, bobdist = 0; vec swaydir(0, 0, 0), swaypush(0, 0, 0); @@ -3057,11 +3057,17 @@ namespace game else curfov = float(fov()); } + float zoomsens() + { + if (focus == player1 && inzoom() && zoomsensitivity > 0) + return (1.f-((zoomlevel+1)/float(zoomlevels+2)))*zoomsensitivity; + else + return 1.f; + } + VAR(0, mouseoverride, 0, 0, 3); bool mousemove(int dx, int dy, int x, int y, int w, int h) { - #define mousesens(a,b,c) ((float(a)/float(b))*c) - if(mouseoverride&2 || (!mouseoverride && hud::hasinput(true))) { float mousemovex = mousesens(dx, w, mousesensitivity); @@ -3083,7 +3089,7 @@ namespace game physent *d = (!gs_playing(gamestate) || player1->state >= CS_SPECTATOR) && (focus == player1 || followaim()) ? camera1 : (allowmove(player1) ? player1 : NULL); if(d) { - float scale = (focus == player1 && inzoom() && zoomsensitivity > 0 ? (1.f-((zoomlevel+1)/float(zoomlevels+2)))*zoomsensitivity : 1.f)*sensitivity; + float scale = zoomsens()*sensitivity; d->yaw += mousesens(dx, sensitivityscale, yawsensitivity*scale); d->pitch -= mousesens(dy, sensitivityscale, pitchsensitivity*scale*(mouseinvert ? -1.f : 1.f)); fixrange(d->yaw, d->pitch); diff --git a/src/game/game.h b/src/game/game.h index 131a412a2..e92718c08 100644 --- a/src/game/game.h +++ b/src/game/game.h @@ -1246,9 +1246,13 @@ template inline void flashcolourf(T &r, T &g, T &b, T &f, T br, T bg, T f += (bf-f)*amt; } +#define mousesens(a,b,c) ((float(a)/float(b))*c) + namespace game { extern int gamestate, gamemode, mutators; + + extern float zoomsens(); } #define AFFINITYPOS(n) \ namespace n \ @@ -2775,6 +2779,8 @@ namespace physics extern float getwaterextinguish(int mat); extern float getwaterextinguishscale(int mat); + + extern void doaction(int type, bool down); } #define LIQUIDPHYS(name,mat) ((mat&MATF_VOLUME) == MAT_LAVA ? physics::getlava##name(mat)*physics::getlava##name##scale(mat) : physics::getwater##name(mat)*physics::getwater##name##scale(mat)) #define LIQUIDVAR(name,mat) ((mat&MATF_VOLUME) == MAT_LAVA ? physics::getlava##name(mat) : physics::getwater##name(mat)) diff --git a/src/game/physics.cpp b/src/game/physics.cpp index 628f25871..7aa7077bc 100644 --- a/src/game/physics.cpp +++ b/src/game/physics.cpp @@ -1,4 +1,5 @@ #include "game.h" +#include "controller.h" namespace physics { FVAR(IDF_MAP, stairheight, 0, 4.1f, 1000); @@ -71,6 +72,7 @@ namespace physics #define imov(name,v,u,d,s,os) \ void do##name(bool down) \ { \ + controller::lastmovementwaskeyboard = true; \ game::player1->s = down; \ int dir = game::player1->s ? d : (game::player1->os ? -(d) : 0); \ game::player1->v = dir; \