Skip to content

Steam Input API core support#1695

Open
RobertCochran wants to merge 26 commits intoredeclipse:masterfrom
RobertCochran:siapi
Open

Steam Input API core support#1695
RobertCochran wants to merge 26 commits intoredeclipse:masterfrom
RobertCochran:siapi

Conversation

@RobertCochran
Copy link

WIP for Steam Input support, as requested on Discord.

There are several things that don't work, aren't implemented the way I'm supposed to, etc.

Perhaps the most important thing to note is that I broke the build for the Red Eclipse server. That needs addressing but it's not short-term important.

@RobertCochran RobertCochran requested a review from a team as a code owner January 28, 2026 06:36
if(d)
{
float scale = (focus == player1 && inzoom() && zoomsensitivity > 0 ? (1.f-((zoomlevel+1)/float(zoomlevels+2)))*zoomsensitivity : 1.f)*sensitivity;
float scale = zoomsens()*sensitivity;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The zoom scaling code got factored out into a helper function so that it could be used inside the controller code.

@qreeves qreeves marked this pull request as draft January 28, 2026 08:18
@qreeves qreeves requested review from qreeves and removed request for a team January 28, 2026 08:18
@qreeves qreeves added this to the 2.1.0 milestone Jan 28, 2026
@Jigoku
Copy link
Member

Jigoku commented Jan 28, 2026

Perhaps the most important thing to note is that I broke the build for the Red Eclipse server. That needs addressing but it's not short-term important.

For this, Looks like you need to add game/controller.o to CLIENT_OBJS in the Makefile so that the compiler can link it 👍

@RobertCochran
Copy link
Author

There's been some more stuff since I posted

Copy link
Author

@RobertCochran RobertCochran Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settings menu item for glyph prompts. 'Automatic' is probably what most people want (dynamically changes glyphs based on last input), but - as is considered good practice - there are options to explicitly lock glyphs if needed. Because Steam Input handles different hardware types for us, the only glyph distinction we need make is KB/M vs controller. This is technically an interface thing, but I feel like it makes more sense to have it together with the controls options.

KNOWN ISSUE 1: The box for this is weirdly not as wide as the other boxes in the menu. I probably copied a setting I didn't intend.
KNOWN ISSUE 2: This box needs to be hidden for non-Steam builds since it will be connected to a variable that will do nothing without SIAPI support.

It may not be worth fiddling with this much because I still need to add a button to open up the controller configurator interface provided by Steam.

Comment on lines 19 to 39
// Steam Input API controller actions
keymap -20 SIAPI_PRIMARY
keymap -21 SIAPI_SECONDARY
keymap -22 SIAPI_RELOAD
keymap -23 SIAPI_USE
keymap -24 SIAPI_JUMP
keymap -25 SIAPI_WALK
keymap -26 SIAPI_CROUCH
keymap -27 SIAPI_SPECIAL
keymap -28 SIAPI_DROP
keymap -29 SIAPI_AFFINITY
keymap -30 SIAPI_DASH
keymap -31 SIAPI_NEXT_WEAPON
keymap -32 SIAPI_PREVIOUS_WEAPON
keymap -33 SIAPI_PRIMARY_WEAPON
keymap -34 SIAPI_SECONDARY_WEAPON
keymap -35 SIAPI_WHEEL_SELECT
keymap -36 SIAPI_CHANGE_LOADOUT
keymap -37 SIAPI_SCOREBOARD
keymap -38 SIAPI_SUICIDE
keymap -39 SIAPI_MENU
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the SIAPI actions to the keymap, as requested. Because only the SIAPI support code should hit these 'keys' in the keymap and there is an enum to map the codes to nice names in the code, this keymap range can be moved theoretically at any time without risk of breaking anything.

Comment on lines 134 to 159
// Steam Input controller binds; these actions _should not be rebound_ - do it
// through the Steam Input configuration interface
bind SIAPI_PRIMARY [ primary ]
specbind SIAPI_PRIMARY [ spectate 0 ]
bind SIAPI_SECONDARY [ secondary ]
specbind SIAPI_SECONDARY [ spectate 0 ]
bind SIAPI_WHEEL_SELECT [ game_hud_piemenu_open_weapsel_key ] // Currently broken
bind SIAPI_NEXT_WEAPON [ universaldelta 1 ]
bind SIAPI_PREVIOUS_WEAPON [ universaldelta -1 ]
bind SIAPI_PRIMARY_WEAPON [ weapon (weapload 0) 1 ]
bind SIAPI_SECONDARY_WEAPON [ weapon (weapload 1) 1 ]
bind SIAPI_MENU [ uitoggle ]
bind SIAPI_JUMP [ jump ]
specbind SIAPI_JUMP [ specmodeswitch ]
bind SIAPI_SPECIAL [ special ]
bind SIAPI_CROUCH [ crouch ]
bind SIAPI_USE [ use ]
specbind SIAPI_USE [ spectate 0 ]
bind SIAPI_RELOAD [ reload ]
specbind SIAPI_RELOAD [ specmodeswitch ]
bind SIAPI_DROP [ drop ]
bind SIAPI_AFFINITY [ affinity ]
bind SIAPI_SCOREBOARD [ showscores ]
bind SIAPI_CHANGE_LOADOUT [ gameui_player_show_loadout ]
bind SIAPI_SUICIDE [ suicide ]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bind the SIAPI actions. You are not supposed to rebind these actions in-engine; you are intended to use the Steam Input configurator tool and change things there.

Comment on lines 745 to 756
struct textkey
{
char *name, *file;
Texture *tex;
textkey() : name(NULL), file(NULL), tex(NULL) {}
textkey(char *n, char *f, Texture *t) : name(newstring(n)), file(newstring(f)), tex(t) {}
~textkey()
{
DELETEA(name);
DELETEA(file);
}
};
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moved to rendertext.h because I needed the struct to be available elsewhere.

Comment on lines 47 to 61
bool shouldkeepkey(const char *str)
{
bool is_siapi_textkey = controller::is_siapi_textkey(str);
switch (textkeyimagepreference) {
case tkip_automatic:
return controller::lastinputwassiapi ? is_siapi_textkey : !is_siapi_textkey;
case tkip_kbm:
return !is_siapi_textkey;
case tkip_controller:
return is_siapi_textkey;
case tkip_both:
return true;
};
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pursuant to above variable, there is now support in the textkey system to filter out glyphs

das->tk->file = NULL; // we don't use this here
das->tk->name = newstring(str);
}
// We have to check if the origin has changed, and if so, reload the texture
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://partner.steamgames.com/doc/features/steam_controller/getting_started_for_devs says that

Please note that the user can change their configuration at any time. To help with this, we've ensured the ISteamInput::GetDigitalActionOrigins and ISteamInput::GetAnalogActionOrigins functions are extremely cheap to call.

When you display an onscreen prompt, don't cache the origins and keep displaying the first results. Instead, we recommend you re-gather the origins each frame, and display the matching prompts. That way, if a user decides to change their configuration as a result of seeing the prompt, when they return from the configuration screen the prompts will automatically update to match the new origins.

In order to honor this best practice, textkeys related to SIAPI actions are handled differently and not cached in the same way that regular KB/M ones are.

public:
InputDigitalActionHandle_t handle = -1;
int keymap_id = -1;
// Should be more than one origin eventually!
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SIAPI actions can have up to 8 origins (which is SIAPIese for 'the button this action is bound to'), but this implementation doesn't yet support actually using them all. It's going to require a non-trivial refactoring of the textkey rendering system to support this.

Comment on lines +800 to +815
vector<textkey *> findtextkeys(const char *str)
{
// SIAPI actions have special handling because there are several fundamental
// differences between SIAPI textkeys and KB/M textkeys
if(controller::is_siapi_textkey(str)) return controller::get_siapi_textkeys(str);

textkey *tk = findtextkey_common(str, textkeys, NULL);

// Should probably just arrange to have this vector be 1 long at
// initialization, but I don't know how to do this...
if(!_findtextkeys_container.capacity()) _findtextkeys_container.add(tk);
else _findtextkeys_container[0] = tk;

return _findtextkeys_container;

}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was redone to return a vector of textkeys instead of a single textkey. This is necessary because Steam Input allows a single action to have multiple origins, so that codepath could potentially have more than one textkey it needs to return. The pure KB/M path only uses its vector as a wrapper and will only ever return 1 textkey.

};

#define WINSTYLE_ENUM(en, um) en(um, Normal, NORMAL) en(um, Tool Tip, TOOLTIP) en(um, Popup, POPUP) en(um, Crosshair, CROSSHAIR) en(um, Cursor, CURSOR) en(um, Max, MAX)
#define WINSTYLE_ENUM(en, um) en(um, Normal, NORMAL) en(um, Pie, PIE) en(um, Gameplay, GAMEPLAY) en(um, Tool Tip, TOOLTIP) en(um, Popup, POPUP) en(um, Crosshair, CROSSHAIR) en(um, Cursor, CURSOR) en(um, Max, MAX)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are new window styles for 'gameplay' and 'pie'. These window styles aren't supposed to affect rendering/handling, but are context markers that tell the controller code how to handle certain scenarios.

Comment on lines +2060 to +2068
bool menuisgameplay()
{
if(surfaceinput == type) loopwindows(w,
{
if(!w->visible || !checkexclusive(w) || w->winstyle >= WINSTYLE_CROSSHAIR) continue;
if(!(w->state&STATE_HIDDEN) && (w->winstyle == WINSTYLE_PIE || w->winstyle == WINSTYLE_GAMEPLAY)) return true;
});
return false;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function and its sibling are possibly too dumb, but any more complicated logic and they would just not work correctly. This implementation is good enough for my needs, but making it smarter would be good.

VAR(0, mouseoverride, 0, 0, 3);
// FIXME: We take x and y parameters but don't use them. A relic of an
// earlier implementation? Delete if possible.
bool mousemove(float dx, float dy, int x, int y, int w, int h, bool fromcontroller)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was changed to distinguish input coming from the mouse and input coming from controllers. The biggest notable difference is that controller inputs deliberately ignore in-engine sensitivity settings and rely entirely on SIAPI sensitivity settings (see the various comments explaining why I do this).

Comment on lines +3134 to +3141
void resetplayerpitch()
{
if(!gs_waiting(gamestate) && (mouseoverride&1 || (!mouseoverride && !tvmode())))
{
if((!gs_playing(gamestate) || player1->state >= CS_SPECTATOR && (focus == player1 || followaim()))) return;
if(allowmove(player1)) player1->pitch = 0.0f;
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting the player pitch was implemented as a 'proper' interface instead of directly accessing the player, which I was told I shouldn't have been doing.

return data.bState;
}

class digital_action_state
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be a struct instead?


digital_action_state *get_das_for_keymap_name(const char *str)
{
// This function is awful, redo to be smarter
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has got to be a better way. Is there a hashmap or something we can use?

Comment on lines +616 to +633
#else /* defined(USE_STEAM) */
void update_from_controller()
{
return;
}

bool is_siapi_textkey(const char *str)
{
return false;
}

vector <textkey *> get_siapi_textkeys(const char *str)
{
return textkeyvec;
}

ICOMMAND(0, showsiapibindpanel, "", (), { return; });
#endif /* defined(USE_STEAM) */
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are suitable dummy functions exported in non-Steam builds

@RobertCochran RobertCochran changed the title WIP: Initial parts of Steam Input API support Steam Input API core support Feb 15, 2026
@RobertCochran RobertCochran marked this pull request as ready for review February 15, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants