1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#include "SDL_joystick_c.h"
24#include "SDL_steam_virtual_gamepad.h"
25
26#ifdef SDL_PLATFORM_WIN32
27#include "../core/windows/SDL_windows.h"
28#else
29#include <sys/types.h>
30#include <sys/stat.h>
31#endif
32
33#define SDL_HINT_STEAM_VIRTUAL_GAMEPAD_INFO_FILE "SteamVirtualGamepadInfo"
34
35static char *SDL_steam_virtual_gamepad_info_file SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
36static Uint64 SDL_steam_virtual_gamepad_info_file_mtime SDL_GUARDED_BY(SDL_joystick_lock) = 0;
37static Uint64 SDL_steam_virtual_gamepad_info_check_time SDL_GUARDED_BY(SDL_joystick_lock) = 0;
38static SDL_SteamVirtualGamepadInfo **SDL_steam_virtual_gamepad_info SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
39static int SDL_steam_virtual_gamepad_info_count SDL_GUARDED_BY(SDL_joystick_lock) = 0;
40
41
42static Uint64 GetFileModificationTime(const char *file)
43{
44 Uint64 modification_time = 0;
45
46#ifdef SDL_PLATFORM_WIN32
47 WCHAR *wFile = WIN_UTF8ToStringW(file);
48 if (wFile) {
49 HANDLE hFile = CreateFileW(wFile, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
50 if (hFile != INVALID_HANDLE_VALUE) {
51 FILETIME last_write_time;
52 if (GetFileTime(hFile, NULL, NULL, &last_write_time)) {
53 modification_time = last_write_time.dwHighDateTime;
54 modification_time <<= 32;
55 modification_time |= last_write_time.dwLowDateTime;
56 }
57 CloseHandle(hFile);
58 }
59 SDL_free(wFile);
60 }
61#else
62 struct stat sb;
63
64 if (stat(file, &sb) == 0) {
65 modification_time = (Uint64)sb.st_mtime;
66 }
67#endif
68 return modification_time;
69}
70
71static void SDL_FreeSteamVirtualGamepadInfo(void)
72{
73 int i;
74
75 SDL_AssertJoysticksLocked();
76
77 for (i = 0; i < SDL_steam_virtual_gamepad_info_count; ++i) {
78 SDL_SteamVirtualGamepadInfo *entry = SDL_steam_virtual_gamepad_info[i];
79 if (entry) {
80 SDL_free(entry->name);
81 SDL_free(entry);
82 }
83 }
84 SDL_free(SDL_steam_virtual_gamepad_info);
85 SDL_steam_virtual_gamepad_info = NULL;
86 SDL_steam_virtual_gamepad_info_count = 0;
87}
88
89static void AddVirtualGamepadInfo(int slot, SDL_SteamVirtualGamepadInfo *info)
90{
91 SDL_SteamVirtualGamepadInfo *new_info;
92
93 SDL_AssertJoysticksLocked();
94
95 if (slot < 0) {
96 return;
97 }
98
99 if (slot >= SDL_steam_virtual_gamepad_info_count) {
100 SDL_SteamVirtualGamepadInfo **slots = (SDL_SteamVirtualGamepadInfo **)SDL_realloc(SDL_steam_virtual_gamepad_info, (slot + 1)*sizeof(*SDL_steam_virtual_gamepad_info));
101 if (!slots) {
102 return;
103 }
104 while (SDL_steam_virtual_gamepad_info_count <= slot) {
105 slots[SDL_steam_virtual_gamepad_info_count++] = NULL;
106 }
107 SDL_steam_virtual_gamepad_info = slots;
108 }
109
110 if (SDL_steam_virtual_gamepad_info[slot]) {
111 // We already have this slot info
112 return;
113 }
114
115 new_info = (SDL_SteamVirtualGamepadInfo *)SDL_malloc(sizeof(*new_info));
116 if (!new_info) {
117 return;
118 }
119 SDL_copyp(new_info, info);
120 SDL_steam_virtual_gamepad_info[slot] = new_info;
121 SDL_zerop(info);
122}
123
124void SDL_InitSteamVirtualGamepadInfo(void)
125{
126 const char *file;
127
128 SDL_AssertJoysticksLocked();
129
130 // The file isn't available inside the macOS sandbox
131 if (SDL_GetSandbox() == SDL_SANDBOX_MACOS) {
132 return;
133 }
134
135 file = SDL_GetHint(SDL_HINT_STEAM_VIRTUAL_GAMEPAD_INFO_FILE);
136 if (file && *file) {
137 SDL_steam_virtual_gamepad_info_file = SDL_strdup(file);
138 }
139 SDL_UpdateSteamVirtualGamepadInfo();
140}
141
142bool SDL_SteamVirtualGamepadEnabled(void)
143{
144 SDL_AssertJoysticksLocked();
145
146 return (SDL_steam_virtual_gamepad_info != NULL);
147}
148
149bool SDL_UpdateSteamVirtualGamepadInfo(void)
150{
151 const int UPDATE_CHECK_INTERVAL_MS = 3000;
152 Uint64 now;
153 Uint64 mtime;
154 char *data, *end, *next, *line, *value;
155 size_t size;
156 int slot, new_slot;
157 SDL_SteamVirtualGamepadInfo info;
158
159 SDL_AssertJoysticksLocked();
160
161 if (!SDL_steam_virtual_gamepad_info_file) {
162 return false;
163 }
164
165 now = SDL_GetTicks();
166 if (SDL_steam_virtual_gamepad_info_check_time &&
167 now < (SDL_steam_virtual_gamepad_info_check_time + UPDATE_CHECK_INTERVAL_MS)) {
168 return false;
169 }
170 SDL_steam_virtual_gamepad_info_check_time = now;
171
172 mtime = GetFileModificationTime(SDL_steam_virtual_gamepad_info_file);
173 if (mtime == 0 || mtime == SDL_steam_virtual_gamepad_info_file_mtime) {
174 return false;
175 }
176
177 data = (char *)SDL_LoadFile(SDL_steam_virtual_gamepad_info_file, &size);
178 if (!data) {
179 return false;
180 }
181
182 SDL_FreeSteamVirtualGamepadInfo();
183
184 slot = -1;
185 SDL_zero(info);
186
187 for (next = data, end = data + size; next < end; ) {
188 while (next < end && (*next == '\0' || *next == '\r' || *next == '\n')) {
189 ++next;
190 }
191
192 line = next;
193
194 while (next < end && (*next != '\r' && *next != '\n')) {
195 ++next;
196 }
197 *next = '\0';
198
199 if (SDL_sscanf(line, "[slot %d]", &new_slot) == 1) {
200 if (slot >= 0) {
201 AddVirtualGamepadInfo(slot, &info);
202 }
203 slot = new_slot;
204 } else {
205 value = SDL_strchr(line, '=');
206 if (value) {
207 *value++ = '\0';
208
209 if (SDL_strcmp(line, "name") == 0) {
210 SDL_free(info.name);
211 info.name = SDL_strdup(value);
212 } else if (SDL_strcmp(line, "VID") == 0) {
213 info.vendor_id = (Uint16)SDL_strtoul(value, NULL, 0);
214 } else if (SDL_strcmp(line, "PID") == 0) {
215 info.product_id = (Uint16)SDL_strtoul(value, NULL, 0);
216 } else if (SDL_strcmp(line, "type") == 0) {
217 info.type = SDL_GetGamepadTypeFromString(value);
218 } else if (SDL_strcmp(line, "handle") == 0) {
219 info.handle = (Uint64)SDL_strtoull(value, NULL, 0);
220 }
221 }
222 }
223 }
224 if (slot >= 0) {
225 AddVirtualGamepadInfo(slot, &info);
226 }
227 SDL_free(info.name);
228 SDL_free(data);
229
230 SDL_steam_virtual_gamepad_info_file_mtime = mtime;
231
232 return true;
233}
234
235const SDL_SteamVirtualGamepadInfo *SDL_GetSteamVirtualGamepadInfo(int slot)
236{
237 SDL_AssertJoysticksLocked();
238
239 if (slot < 0 || slot >= SDL_steam_virtual_gamepad_info_count) {
240 return NULL;
241 }
242 return SDL_steam_virtual_gamepad_info[slot];
243}
244
245void SDL_QuitSteamVirtualGamepadInfo(void)
246{
247 SDL_AssertJoysticksLocked();
248
249 if (SDL_steam_virtual_gamepad_info_file) {
250 SDL_FreeSteamVirtualGamepadInfo();
251 SDL_free(SDL_steam_virtual_gamepad_info_file);
252 SDL_steam_virtual_gamepad_info_file = NULL;
253 }
254}
255