From fe01b9881bb963e957f7863a01f9437fe3509d52 Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Thu, 21 Dec 2023 01:32:49 +1100 Subject: [PATCH] Joystick mapping support for MacOS using SDL2 --- Makefile.in | 2 + aclocal.m4 | 80 +----- configure | 92 +++++- configure.ac | 5 + share/Makefile.in | 2 + src/Makefile.am | 11 +- src/Makefile.in | 20 +- src/widgets/joystick_sdl.c | 560 +++++++++++++++++++++++++++++++++++++ 8 files changed, 685 insertions(+), 87 deletions(-) create mode 100644 src/widgets/joystick_sdl.c diff --git a/Makefile.in b/Makefile.in index c998c1a..1cb1aae 100644 --- a/Makefile.in +++ b/Makefile.in @@ -278,6 +278,8 @@ PATH_SEPARATOR = @PATH_SEPARATOR@ PKG_CONFIG = @PKG_CONFIG@ PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +SDL2_CFLAGS = @SDL2_CFLAGS@ +SDL2_LIBS = @SDL2_LIBS@ SET_MAKE = @SET_MAKE@ SHELL = @SHELL@ STRIP = @STRIP@ diff --git a/aclocal.m4 b/aclocal.m4 index 5b12ff7..174327b 100644 --- a/aclocal.m4 +++ b/aclocal.m4 @@ -21,7 +21,7 @@ If you have problems, you may need to regenerate the build system entirely. To do so, use the procedure documented by the package, typically 'autoreconf'.])]) # pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- -# serial 11 (pkg-config-0.29.1) +# serial 12 (pkg-config-0.29.2) dnl Copyright © 2004 Scott James Remnant . dnl Copyright © 2012-2015 Dan Nicholson @@ -63,7 +63,7 @@ dnl dnl See the "Since" comment for each macro you use to see what version dnl of the macros you require. m4_defun([PKG_PREREQ], -[m4_define([PKG_MACROS_VERSION], [0.29.1]) +[m4_define([PKG_MACROS_VERSION], [0.29.2]) m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) ])dnl PKG_PREREQ @@ -164,7 +164,7 @@ AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl pkg_failed=no -AC_MSG_CHECKING([for $1]) +AC_MSG_CHECKING([for $2]) _PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) _PKG_CONFIG([$1][_LIBS], [libs], [$2]) @@ -174,11 +174,11 @@ and $1[]_LIBS to avoid the need to call pkg-config. See the pkg-config man page for more details.]) if test $pkg_failed = yes; then - AC_MSG_RESULT([no]) + AC_MSG_RESULT([no]) _PKG_SHORT_ERRORS_SUPPORTED if test $_pkg_short_errors_supported = yes; then $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` - else + else $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` fi # Put the nasty error message in config.log where it belongs @@ -195,7 +195,7 @@ installed software in a non-standard prefix. _PKG_TEXT])[]dnl ]) elif test $pkg_failed = untried; then - AC_MSG_RESULT([no]) + AC_MSG_RESULT([no]) m4_default([$4], [AC_MSG_FAILURE( [The pkg-config script could not be found or is too old. Make sure it is in your PATH or set the PKG_CONFIG environment variable to the full @@ -296,74 +296,6 @@ AS_VAR_COPY([$1], [pkg_cv_][$1]) AS_VAR_IF([$1], [""], [$5], [$4])dnl ])dnl PKG_CHECK_VAR -dnl PKG_WITH_MODULES(VARIABLE-PREFIX, MODULES, -dnl [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND], -dnl [DESCRIPTION], [DEFAULT]) -dnl ------------------------------------------ -dnl -dnl Prepare a "--with-" configure option using the lowercase -dnl [VARIABLE-PREFIX] name, merging the behaviour of AC_ARG_WITH and -dnl PKG_CHECK_MODULES in a single macro. -AC_DEFUN([PKG_WITH_MODULES], -[ -m4_pushdef([with_arg], m4_tolower([$1])) - -m4_pushdef([description], - [m4_default([$5], [build with ]with_arg[ support])]) - -m4_pushdef([def_arg], [m4_default([$6], [auto])]) -m4_pushdef([def_action_if_found], [AS_TR_SH([with_]with_arg)=yes]) -m4_pushdef([def_action_if_not_found], [AS_TR_SH([with_]with_arg)=no]) - -m4_case(def_arg, - [yes],[m4_pushdef([with_without], [--without-]with_arg)], - [m4_pushdef([with_without],[--with-]with_arg)]) - -AC_ARG_WITH(with_arg, - AS_HELP_STRING(with_without, description[ @<:@default=]def_arg[@:>@]),, - [AS_TR_SH([with_]with_arg)=def_arg]) - -AS_CASE([$AS_TR_SH([with_]with_arg)], - [yes],[PKG_CHECK_MODULES([$1],[$2],$3,$4)], - [auto],[PKG_CHECK_MODULES([$1],[$2], - [m4_n([def_action_if_found]) $3], - [m4_n([def_action_if_not_found]) $4])]) - -m4_popdef([with_arg]) -m4_popdef([description]) -m4_popdef([def_arg]) - -])dnl PKG_WITH_MODULES - -dnl PKG_HAVE_WITH_MODULES(VARIABLE-PREFIX, MODULES, -dnl [DESCRIPTION], [DEFAULT]) -dnl ----------------------------------------------- -dnl -dnl Convenience macro to trigger AM_CONDITIONAL after PKG_WITH_MODULES -dnl check._[VARIABLE-PREFIX] is exported as make variable. -AC_DEFUN([PKG_HAVE_WITH_MODULES], -[ -PKG_WITH_MODULES([$1],[$2],,,[$3],[$4]) - -AM_CONDITIONAL([HAVE_][$1], - [test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"]) -])dnl PKG_HAVE_WITH_MODULES - -dnl PKG_HAVE_DEFINE_WITH_MODULES(VARIABLE-PREFIX, MODULES, -dnl [DESCRIPTION], [DEFAULT]) -dnl ------------------------------------------------------ -dnl -dnl Convenience macro to run AM_CONDITIONAL and AC_DEFINE after -dnl PKG_WITH_MODULES check. HAVE_[VARIABLE-PREFIX] is exported as make -dnl and preprocessor variable. -AC_DEFUN([PKG_HAVE_DEFINE_WITH_MODULES], -[ -PKG_HAVE_WITH_MODULES([$1],[$2],[$3],[$4]) - -AS_IF([test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"], - [AC_DEFINE([HAVE_][$1], 1, [Enable ]m4_tolower([$1])[ support])]) -])dnl PKG_HAVE_DEFINE_WITH_MODULES - # Copyright (C) 2002-2021 Free Software Foundation, Inc. # # This file is free software; the Free Software Foundation diff --git a/configure b/configure index 33904a2..6fdde23 100755 --- a/configure +++ b/configure @@ -650,6 +650,8 @@ am__EXEEXT_TRUE LTLIBOBJS LIBOBJS DEFINES +SDL2_LIBS +SDL2_CFLAGS GTK_LIBS GTK_CFLAGS PKG_CONFIG_LIBDIR @@ -774,7 +776,9 @@ PKG_CONFIG PKG_CONFIG_PATH PKG_CONFIG_LIBDIR GTK_CFLAGS -GTK_LIBS' +GTK_LIBS +SDL2_CFLAGS +SDL2_LIBS' # Initialize some variables set by options. @@ -1424,6 +1428,8 @@ Some influential environment variables: path overriding pkg-config's built-in search path GTK_CFLAGS C compiler flags for GTK, overriding pkg-config GTK_LIBS linker flags for GTK, overriding pkg-config + SDL2_CFLAGS C compiler flags for SDL2, overriding pkg-config + SDL2_LIBS linker flags for SDL2, overriding pkg-config Use these variables to override the choices made by `configure' or to help it to find libraries and programs with nonstandard names/locations. @@ -4886,8 +4892,8 @@ fi pkg_failed=no -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for GTK" >&5 -printf %s "checking for GTK... " >&6; } +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gtk+-3.0 >= 3.16 glib-2.0 >= 2.42" >&5 +printf %s "checking for gtk+-3.0 >= 3.16 glib-2.0 >= 2.42... " >&6; } if test -n "$GTK_CFLAGS"; then pkg_cv_GTK_CFLAGS="$GTK_CFLAGS" @@ -4927,7 +4933,7 @@ fi if test $pkg_failed = yes; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then @@ -4945,7 +4951,7 @@ fi as_fn_error $? "Mednaffe needs GTK+ >= 3.16 development libraries" "$LINENO" 5 elif test $pkg_failed = untried; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 printf "%s\n" "no" >&6; } as_fn_error $? "Mednaffe needs GTK+ >= 3.16 development libraries" "$LINENO" 5 else @@ -4956,6 +4962,82 @@ printf "%s\n" "yes" >&6; } { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Checking for GTK+ 3 development libraries" >&5 printf "%s\n" "$as_me: Checking for GTK+ 3 development libraries" >&6;} fi +case $host_os in + darwin*) + +pkg_failed=no +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for SDL2 >= 2.0.6" >&5 +printf %s "checking for SDL2 >= 2.0.6... " >&6; } + +if test -n "$SDL2_CFLAGS"; then + pkg_cv_SDL2_CFLAGS="$SDL2_CFLAGS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"SDL2 >= 2.0.6\""; } >&5 + ($PKG_CONFIG --exists --print-errors "SDL2 >= 2.0.6") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_SDL2_CFLAGS=`$PKG_CONFIG --cflags "SDL2 >= 2.0.6" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes +fi + else + pkg_failed=untried +fi +if test -n "$SDL2_LIBS"; then + pkg_cv_SDL2_LIBS="$SDL2_LIBS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"SDL2 >= 2.0.6\""; } >&5 + ($PKG_CONFIG --exists --print-errors "SDL2 >= 2.0.6") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_SDL2_LIBS=`$PKG_CONFIG --libs "SDL2 >= 2.0.6" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes +fi + else + pkg_failed=untried +fi + + + +if test $pkg_failed = yes; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + +if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then + _pkg_short_errors_supported=yes +else + _pkg_short_errors_supported=no +fi + if test $_pkg_short_errors_supported = yes; then + SDL2_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "SDL2 >= 2.0.6" 2>&1` + else + SDL2_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "SDL2 >= 2.0.6" 2>&1` + fi + # Put the nasty error message in config.log where it belongs + echo "$SDL2_PKG_ERRORS" >&5 + + as_fn_error $? "Mednaffe darwin needs SDL2 >= 2.0.6 development libraries" "$LINENO" 5 +elif test $pkg_failed = untried; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + as_fn_error $? "Mednaffe darwin needs SDL2 >= 2.0.6 development libraries" "$LINENO" 5 +else + SDL2_CFLAGS=$pkg_cv_SDL2_CFLAGS + SDL2_LIBS=$pkg_cv_SDL2_LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Checking for SDL2 development libraries" >&5 +printf "%s\n" "$as_me: Checking for SDL2 development libraries" >&6;} +fi + ;; +esac diff --git a/configure.ac b/configure.ac index 035ee06..8530961 100644 --- a/configure.ac +++ b/configure.ac @@ -65,6 +65,11 @@ else fi PKG_CHECK_MODULES([GTK],[gtk+-3.0 >= 3.16 glib-2.0 >= 2.42], AC_MSG_NOTICE([Checking for GTK+ 3 development libraries]), AC_MSG_ERROR(Mednaffe needs GTK+ >= 3.16 development libraries)) +case $host_os in + darwin*) + PKG_CHECK_MODULES([SDL2],[SDL2 >= 2.0.6], AC_MSG_NOTICE([Checking for SDL2 development libraries]), AC_MSG_ERROR(Mednaffe darwin needs SDL2 >= 2.0.6 development libraries)) + ;; +esac AC_SUBST(DEFINES) diff --git a/share/Makefile.in b/share/Makefile.in index 2022093..cffb5e8 100644 --- a/share/Makefile.in +++ b/share/Makefile.in @@ -200,6 +200,8 @@ PATH_SEPARATOR = @PATH_SEPARATOR@ PKG_CONFIG = @PKG_CONFIG@ PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +SDL2_CFLAGS = @SDL2_CFLAGS@ +SDL2_LIBS = @SDL2_LIBS@ SET_MAKE = @SET_MAKE@ SHELL = @SHELL@ STRIP = @STRIP@ diff --git a/src/Makefile.am b/src/Makefile.am index 8a0e410..f87c2b1 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -45,7 +45,7 @@ if WINDOWS LIBS +=../share/win/mednaffe.res -ldxguid -ldinput else if MAC - mednaffe_SOURCES+= widgets/joystick_dummy.c + mednaffe_SOURCES+= widgets/joystick_sdl.c else mednaffe_SOURCES+= widgets/joystick_dummy.c override CFLAGS +=-Wl,-export-dynamic @@ -54,7 +54,12 @@ endif endif -AM_CPPFLAGS = @GTK_CFLAGS@ -LDADD = @GTK_LIBS@ +if MAC + AM_CPPFLAGS = @GTK_CFLAGS@ @SDL2_CFLAGS@ + LDADD = @GTK_LIBS@ @SDL2_LIBS@ +else + AM_CPPFLAGS = @GTK_CFLAGS@ + LDADD = @GTK_LIBS@ +endif CLEANFILES = *~ DISTCLEANFILES = .deps/*.P diff --git a/src/Makefile.in b/src/Makefile.in index cb596d3..1522e5e 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -92,7 +92,7 @@ bin_PROGRAMS = mednaffe$(EXEEXT) @LINUX_TRUE@am__append_1 = widgets/joystick_linux.c @LINUX_FALSE@@WINDOWS_TRUE@am__append_2 = win32util.c win32util.h widgets/joystick_windows.c @LINUX_FALSE@@WINDOWS_TRUE@am__append_3 = ../share/win/mednaffe.res -ldxguid -ldinput -@LINUX_FALSE@@MAC_TRUE@@WINDOWS_FALSE@am__append_4 = widgets/joystick_dummy.c +@LINUX_FALSE@@MAC_TRUE@@WINDOWS_FALSE@am__append_4 = widgets/joystick_sdl.c @LINUX_FALSE@@MAC_FALSE@@WINDOWS_FALSE@am__append_5 = widgets/joystick_dummy.c subdir = src ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 @@ -126,13 +126,13 @@ am__mednaffe_SOURCES_DIST = about.c about.h bios.c bios.h logbook.c \ widgets/medspin.c widgets/medspin.h widgets/medwidget.c \ widgets/medwidget.h widgets/menuinput.c widgets/menuinput.h \ widgets/joystick.h widgets/joystick_linux.c win32util.c \ - win32util.h widgets/joystick_windows.c \ + win32util.h widgets/joystick_windows.c widgets/joystick_sdl.c \ widgets/joystick_dummy.c am__dirstamp = $(am__leading_dot)dirstamp @LINUX_TRUE@am__objects_1 = widgets/joystick_linux.$(OBJEXT) @LINUX_FALSE@@WINDOWS_TRUE@am__objects_2 = win32util.$(OBJEXT) \ @LINUX_FALSE@@WINDOWS_TRUE@ widgets/joystick_windows.$(OBJEXT) -@LINUX_FALSE@@MAC_TRUE@@WINDOWS_FALSE@am__objects_3 = widgets/joystick_dummy.$(OBJEXT) +@LINUX_FALSE@@MAC_TRUE@@WINDOWS_FALSE@am__objects_3 = widgets/joystick_sdl.$(OBJEXT) @LINUX_FALSE@@MAC_FALSE@@WINDOWS_FALSE@am__objects_4 = widgets/joystick_dummy.$(OBJEXT) am_mednaffe_OBJECTS = about.$(OBJEXT) bios.$(OBJEXT) logbook.$(OBJEXT) \ mainwindow.$(OBJEXT) manager.$(OBJEXT) mednaffe.$(OBJEXT) \ @@ -179,6 +179,7 @@ am__depfiles_remade = ./$(DEPDIR)/about.Po ./$(DEPDIR)/bios.Po \ widgets/$(DEPDIR)/dialogs.Po \ widgets/$(DEPDIR)/joystick_dummy.Po \ widgets/$(DEPDIR)/joystick_linux.Po \ + widgets/$(DEPDIR)/joystick_sdl.Po \ widgets/$(DEPDIR)/joystick_windows.Po \ widgets/$(DEPDIR)/marshallers.Po \ widgets/$(DEPDIR)/medbiosentry.Po \ @@ -277,6 +278,8 @@ PATH_SEPARATOR = @PATH_SEPARATOR@ PKG_CONFIG = @PKG_CONFIG@ PKG_CONFIG_LIBDIR = @PKG_CONFIG_LIBDIR@ PKG_CONFIG_PATH = @PKG_CONFIG_PATH@ +SDL2_CFLAGS = @SDL2_CFLAGS@ +SDL2_LIBS = @SDL2_LIBS@ SET_MAKE = @SET_MAKE@ SHELL = @SHELL@ STRIP = @STRIP@ @@ -354,8 +357,10 @@ mednaffe_SOURCES = about.c about.h bios.c bios.h logbook.c logbook.h \ widgets/medwidget.h widgets/menuinput.c widgets/menuinput.h \ widgets/joystick.h $(am__append_1) $(am__append_2) \ $(am__append_4) $(am__append_5) -AM_CPPFLAGS = @GTK_CFLAGS@ -LDADD = @GTK_LIBS@ +@MAC_FALSE@AM_CPPFLAGS = @GTK_CFLAGS@ +@MAC_TRUE@AM_CPPFLAGS = @GTK_CFLAGS@ @SDL2_CFLAGS@ +@MAC_FALSE@LDADD = @GTK_LIBS@ +@MAC_TRUE@LDADD = @GTK_LIBS@ @SDL2_LIBS@ CLEANFILES = *~ DISTCLEANFILES = .deps/*.P all: all-am @@ -477,6 +482,8 @@ widgets/joystick_linux.$(OBJEXT): widgets/$(am__dirstamp) \ widgets/$(DEPDIR)/$(am__dirstamp) widgets/joystick_windows.$(OBJEXT): widgets/$(am__dirstamp) \ widgets/$(DEPDIR)/$(am__dirstamp) +widgets/joystick_sdl.$(OBJEXT): widgets/$(am__dirstamp) \ + widgets/$(DEPDIR)/$(am__dirstamp) widgets/joystick_dummy.$(OBJEXT): widgets/$(am__dirstamp) \ widgets/$(DEPDIR)/$(am__dirstamp) @@ -510,6 +517,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/dialogs.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/joystick_dummy.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/joystick_linux.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/joystick_sdl.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/joystick_windows.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/marshallers.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@widgets/$(DEPDIR)/medbiosentry.Po@am__quote@ # am--include-marker @@ -698,6 +706,7 @@ distclean: distclean-am -rm -f widgets/$(DEPDIR)/dialogs.Po -rm -f widgets/$(DEPDIR)/joystick_dummy.Po -rm -f widgets/$(DEPDIR)/joystick_linux.Po + -rm -f widgets/$(DEPDIR)/joystick_sdl.Po -rm -f widgets/$(DEPDIR)/joystick_windows.Po -rm -f widgets/$(DEPDIR)/marshallers.Po -rm -f widgets/$(DEPDIR)/medbiosentry.Po @@ -778,6 +787,7 @@ maintainer-clean: maintainer-clean-am -rm -f widgets/$(DEPDIR)/dialogs.Po -rm -f widgets/$(DEPDIR)/joystick_dummy.Po -rm -f widgets/$(DEPDIR)/joystick_linux.Po + -rm -f widgets/$(DEPDIR)/joystick_sdl.Po -rm -f widgets/$(DEPDIR)/joystick_windows.Po -rm -f widgets/$(DEPDIR)/marshallers.Po -rm -f widgets/$(DEPDIR)/medbiosentry.Po diff --git a/src/widgets/joystick_sdl.c b/src/widgets/joystick_sdl.c new file mode 100644 index 0000000..2b95072 --- /dev/null +++ b/src/widgets/joystick_sdl.c @@ -0,0 +1,560 @@ +/* joystick_sdl.c - Copyright 2023 a dinosaur - SPDX: GPL-3.0-or-later */ +#include "joystick.h" +#include + +// Enable experimental SDL_GameController support +#define ENABLE_SDL_GAMECONTROLLER + +static joy_s *OpenJoystick(int id); +static gchar *ReadJoystick(const gchar *joyid, SDL_Joystick *sdljoy); +static gchar *BuildGuid(const gchar *name, gssize namelen, SDL_Joystick *sdljoy); +static const joy_s *GetJoystickFromGUID(GSList *restrict list, const gchar *id); + +#ifdef ENABLE_SDL_GAMECONTROLLER + +typedef struct _GameController { + SDL_GameController *sdlpad; + SDL_Joystick *sdljoy; + SDL_GameControllerButton map_button[SDL_CONTROLLER_BUTTON_MAX]; + SDL_GameControllerAxis map_axis[SDL_CONTROLLER_AXIS_MAX]; +} GameController; + +static joy_s *OpenGameController(int id); +static gchar *ReadGameController(const gchar *joyid, GameController *restrict pad); +static int IntFromStr(const char *str, long *restrict out); +static gchar *GameControllerButtonDescription(SDL_GameControllerType type, SDL_GameControllerButton button); +static gchar *GameControllerAxisDescription(SDL_GameControllerType type, SDL_GameControllerAxis axis); +static int GetGameControllerMapping(GameController *restrict pad); + +#endif + + +gchar *value_to_text(GSList *listjoy, const gchar *value) +{ + gchar **items = g_strsplit(value, " ", 4); + if (g_strv_length(items) < 3) + { + g_strfreev(items); + return NULL; + } + + const joy_s *joy = GetJoystickFromGUID(listjoy, items[1]); + const gchar *devname = (joy) ? joy->name : "Unknown Device"; + + gchar *text = NULL; +#ifdef ENABLE_SDL_GAMECONTROLLER + if (joy && joy->type == 2) + { + const GameController *pad = joy->data1; + long joyvalue; + if (items[2][0] == 'b' && !IntFromStr(&items[2][7], &joyvalue)) + { + for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; ++i) + if (pad->map_button[i] == joyvalue) + { + const gchar *description = GameControllerButtonDescription(SDL_GameControllerGetType(pad->sdlpad), i); + if (description) + text = g_strconcat("Button ", description, " (", devname, ")", NULL); + break; + } + } + else if (items[2][0] == 'a' && !IntFromStr(&items[2][4], &joyvalue)) + { + for (int i = 0; i < SDL_CONTROLLER_AXIS_MAX; ++i) + if (pad->map_axis[i] == joyvalue) + { + const gchar *description = GameControllerAxisDescription(SDL_GameControllerGetType(pad->sdlpad), i); + if (description) + { + const gchar *findpos = strchr(&items[2][4], '+'); + const gchar *findneg = strchr(&items[2][4], '-'); + const gchar *polarity = ""; + if (findpos && findneg) polarity = findpos < findneg ? findpos : findneg; + else if (findpos) polarity = findpos; + else if (findneg) polarity = findneg; + text = g_strconcat("Axis ", description, polarity, " (", devname, ")", NULL); + } + break; + } + } + } + + if (!text) + { +#endif + if (items[2][0] == 'b') + text = g_strconcat("Button ", &items[2][7], " (", devname, ")", NULL); + else if (items[2][0] == 'a') + text = g_strconcat("Axis ", &items[2][4], " (", devname, ")", NULL); +#ifdef ENABLE_SDL_GAMECONTROLLER + } +#endif + + g_strfreev(items); + return text; +} + +GSList *init_joys(void) +{ + if (SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) + return NULL; + + GSList *joylist = NULL; + const int numjoy = SDL_NumJoysticks(); + for (int i = 0; i < numjoy; ++i) + { +#ifdef ENABLE_SDL_GAMECONTROLLER + joy_s *joy = SDL_IsGameController(i) ? OpenGameController(i) : OpenJoystick(i); +#else + joy_s *joy = OpenJoystick(i); +#endif + if (!joy) + continue; + + joylist = g_slist_append(joylist, joy); + } + return joylist; +} + +void close_joys(GSList *list) +{ + for (GSList *it = list; it != NULL; it = it->next) + { + joy_s *joy = it->data; + if (joy->type == 1) + { + SDL_JoystickClose(joy->data1); + } +#ifdef ENABLE_SDL_GAMECONTROLLER + else if (joy->type == 2) + { + GameController *pad = (GameController*)joy->data1; + SDL_GameControllerClose(pad->sdlpad); + g_free(pad); + } +#endif + g_free(joy->name); + g_free(joy->id); + g_free(joy); + } + + SDL_Quit(); +} + +gchar *read_joys(GSList *list) +{ + SDL_PumpEvents(); + + gchar *read = NULL; + for (GSList *it = list; it != NULL; it = it->next) + { + joy_s *joy = it->data; + if (joy->type == 1) + read = ReadJoystick(joy->id, (SDL_Joystick*)joy->data1); +#ifdef ENABLE_SDL_GAMECONTROLLER + else if (joy->type == 2) + read = ReadGameController(joy->id, (GameController*)joy->data1); +#endif + if (read) + break; + } + return read; +} + +void discard_read(GSList *list) +{ +} + + +joy_s *OpenJoystick(int id) +{ + SDL_Joystick *sdljoy = SDL_JoystickOpen(id); + if (!sdljoy) + return NULL; + + gchar *joyname = NULL, *guid = NULL; + + const char *name = SDL_JoystickName(sdljoy); + if (!name) + goto OpenJoystickError; + const gsize namelen = strlen(name); + if (!(joyname = g_strndup(name, namelen))) + goto OpenJoystickError; + + guid = BuildGuid(name, namelen, sdljoy); + if (!guid) + goto OpenJoystickError; + + joy_s *joy = g_new0(joy_s, 1); + if (!joy) + goto OpenJoystickError; + joy->num = (gint)SDL_JoystickGetDeviceInstanceID(id); + joy->type = 1; + joy->name = joyname; + joy->id = guid; + joy->data1 = (gpointer)sdljoy; + return joy; + +OpenJoystickError: + g_free(guid); + g_free(joyname); + SDL_JoystickClose(sdljoy); + return NULL; +} + +gchar *ReadJoystick(const gchar *joyid, SDL_Joystick *sdljoy) +{ + unsigned numaxes = SDL_JoystickNumAxes(sdljoy); + unsigned numbtns = SDL_JoystickNumButtons(sdljoy); + + for (unsigned i = 0; i < numaxes; ++i) + { + Sint16 zero; + if (SDL_JoystickGetAxisInitialState(sdljoy, i, &zero) == SDL_FALSE) + zero = 0; + + Sint16 axis = SDL_JoystickGetAxis(sdljoy, i); + if (zero <= -0x4000) + { + if (axis >= 0) + return g_strdup_printf("joystick %s abs_%u-+", joyid, i); + } + else if (zero >= 0x4000) + { + if (axis < 0) + return g_strdup_printf("joystick %s abs_%u+-", joyid, i); + } + else if (axis <= -0x4000 || axis >= 0x4000) + { + const gchar polarity = axis < 0 ? '-' : '+'; + return g_strdup_printf("joystick %s abs_%u%c", joyid, i, polarity); + } + } + + for (unsigned i = 0; i < numbtns; ++i) + { + if (SDL_JoystickGetButton(sdljoy, i)) + return g_strdup_printf("joystick %s button_%u", joyid, i); + } + + return NULL; +} + + +gchar *BuildGuid(const gchar *name, gssize namelen, SDL_Joystick *sdljoy) +{ + // Compute MD5 hash of Joystick name + GChecksum *hash = g_checksum_new(G_CHECKSUM_MD5); + if (!hash) + return NULL; + guint8 digest[16]; + gsize digestlen = 16; + g_checksum_update(hash, (guchar*)name, namelen); + g_checksum_get_digest(hash, &digest[0], &digestlen); + g_checksum_free(hash); + + // Build unique ID, a joystick ID is computed by taking the first 64 bits of + // the MD5 hash of the name and appending total axis, buttons, hats, and balls. + gchar *guid = g_malloc(35); + if (!guid) + return NULL; + snprintf(&guid[0], 3, "0x"); + for (int i = 0; i < 8; ++i) + snprintf(&guid[2 + i * 2], 3, "%02x", (unsigned char)digest[i]); + + unsigned numaxes = SDL_JoystickNumAxes(sdljoy); + unsigned numbtns = SDL_JoystickNumButtons(sdljoy); + unsigned numhats = SDL_JoystickNumHats(sdljoy); + unsigned numball = SDL_JoystickNumBalls(sdljoy); + snprintf(&guid[18], 17, "%04x%04x%04x%04x", numaxes, numbtns, numhats, numball); + + return guid; +} + +const joy_s *GetJoystickFromGUID(GSList *restrict list, const gchar *id) +{ + if (!list || !id) + return NULL; + for (GSList *it = list; it != NULL; it = it->next) + { + const joy_s *joy = it->data; + if (g_strcmp0(id, joy->id) == 0) + return joy; + } + return NULL; +} + + +#ifdef ENABLE_SDL_GAMECONTROLLER + +joy_s *OpenGameController(int id) +{ + SDL_GameController *sdlpad = SDL_GameControllerOpen(id); + if (!sdlpad) + return NULL; + + GameController *pad = NULL; + gchar *joyname = NULL, *guid = NULL; + + pad = g_new0(GameController, 1); + pad->sdlpad = sdlpad; + pad->sdljoy = SDL_GameControllerGetJoystick(sdlpad); + for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; ++i) + pad->map_button[i] = SDL_CONTROLLER_BUTTON_INVALID; + for (int i = 0; i < SDL_CONTROLLER_AXIS_MAX; ++i) + pad->map_axis[i] = SDL_CONTROLLER_AXIS_INVALID; + + const char *name = SDL_GameControllerName(sdlpad); + if (!name) + goto OpenGameControllerError; + const gsize namelen = strlen(name); + if (!(joyname = g_strndup(name, namelen))) + goto OpenGameControllerError; + + guid = BuildGuid(name, namelen, pad->sdljoy); + if (!guid || GetGameControllerMapping(pad)) + goto OpenGameControllerError; + + joy_s *joy = g_new0(joy_s, 1); + if (!joy) + goto OpenGameControllerError; + joy->num = (gint)SDL_JoystickGetDeviceInstanceID(id); + joy->type = 2; + joy->name = joyname; + joy->id = guid; + joy->data1 = (gpointer)pad; + return joy; + +OpenGameControllerError: + g_free(guid); + g_free(joyname); + g_free(pad); + SDL_GameControllerClose(sdlpad); + return NULL; +} + +gchar *ReadGameController(const gchar *joyid, GameController *restrict pad) +{ + SDL_GameController *sdlpad = pad->sdlpad; + + for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; ++i) + { + if (SDL_GameControllerGetButton(sdlpad, i) == SDL_PRESSED && pad->map_button[i] > SDL_CONTROLLER_BUTTON_INVALID) + return g_strdup_printf("joystick %s button_%u", joyid, pad->map_button[i]); + } + + for (int i = 0; i < SDL_CONTROLLER_AXIS_MAX; ++i) + { + if (pad->map_axis[i] <= SDL_CONTROLLER_AXIS_INVALID) + continue; + Sint16 axis = SDL_GameControllerGetAxis(sdlpad, i); + if ((i == SDL_CONTROLLER_AXIS_TRIGGERLEFT || i == SDL_CONTROLLER_AXIS_TRIGGERRIGHT) && axis >= 0x4000) + { + return g_strdup_printf("joystick %s abs_%u-+", joyid, pad->map_axis[i]); + } + else if (axis <= -0x4000 || axis >= 0x4000) + { + const gchar polarity = axis < 0 ? '-' : '+'; + return g_strdup_printf("joystick %s abs_%u%c", joyid, pad->map_axis[i], polarity); + } + } + + return NULL; +} + + +int IntFromStr(const char *str, long *restrict out) +{ + errno = 0; + char *end; + long value = strtol(str, &end, 10); + if (end == str) + return 1; + if (errno == ERANGE) + return 2; + *out = value; + return 0; +} + + +gchar *GameControllerButtonDescription(SDL_GameControllerType type, SDL_GameControllerButton button) +{ + if (type == SDL_CONTROLLER_TYPE_PS3 || type == SDL_CONTROLLER_TYPE_PS4 || type == SDL_CONTROLLER_TYPE_PS5) + { + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: return "X"; + case SDL_CONTROLLER_BUTTON_B: return "◯"; + case SDL_CONTROLLER_BUTTON_X: return "◻"; + case SDL_CONTROLLER_BUTTON_Y: return "△"; + case SDL_CONTROLLER_BUTTON_GUIDE: return "PS"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: return "L3"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return "R3"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return "L1"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return "R1"; + case SDL_CONTROLLER_BUTTON_MISC1: return "Mic Mute"; + default: break; + } + } + if (type == SDL_CONTROLLER_TYPE_PS4 || type == SDL_CONTROLLER_TYPE_PS5) + { + switch (button) + { + case SDL_CONTROLLER_BUTTON_START: return "Options"; + case SDL_CONTROLLER_BUTTON_BACK: return type == SDL_CONTROLLER_TYPE_PS4 ? "Share" : "Create"; + default: break; + } + } + if (type == SDL_CONTROLLER_TYPE_XBOX360 || type == SDL_CONTROLLER_TYPE_XBOXONE) + { + switch (button) + { + case SDL_CONTROLLER_BUTTON_LEFTSTICK: return "LS"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return "RS"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return "LB"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return "RB"; + case SDL_CONTROLLER_BUTTON_MISC1: return "Share"; + case SDL_CONTROLLER_BUTTON_PADDLE1: return "P1"; + case SDL_CONTROLLER_BUTTON_PADDLE2: return "P2"; + case SDL_CONTROLLER_BUTTON_PADDLE3: return "P3"; + case SDL_CONTROLLER_BUTTON_PADDLE4: return "P4"; + default: break; + } + } + if (type == SDL_CONTROLLER_TYPE_XBOXONE) + { + switch (button) + { + case SDL_CONTROLLER_BUTTON_BACK: return "View"; + case SDL_CONTROLLER_BUTTON_GUIDE: return "Xbox"; + case SDL_CONTROLLER_BUTTON_START: return "Option"; + default: break; + } + } + + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: return "A"; + case SDL_CONTROLLER_BUTTON_B: return "B"; + case SDL_CONTROLLER_BUTTON_X: return "X"; + case SDL_CONTROLLER_BUTTON_Y: return "Y"; + case SDL_CONTROLLER_BUTTON_BACK: return "Back"; + case SDL_CONTROLLER_BUTTON_GUIDE: return "Guide"; + case SDL_CONTROLLER_BUTTON_START: return "Start"; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: return "Left Stick Press"; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return "Right Stick Press"; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return "Left Shoulder"; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return "Right Shoulder"; + case SDL_CONTROLLER_BUTTON_DPAD_UP: return "D-Pad Up"; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return "D-Pad Down"; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return "D-Pad Left"; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return "D-Pad Right"; + case SDL_CONTROLLER_BUTTON_MISC1: return "Misc 1"; + case SDL_CONTROLLER_BUTTON_PADDLE1: return "Paddle 1"; + case SDL_CONTROLLER_BUTTON_PADDLE2: return "Paddle 2"; + case SDL_CONTROLLER_BUTTON_PADDLE3: return "Paddle 3"; + case SDL_CONTROLLER_BUTTON_PADDLE4: return "Paddle 4"; + case SDL_CONTROLLER_BUTTON_TOUCHPAD: return "Touchpad"; + default: return NULL; + } +} + +gchar *GameControllerAxisDescription(SDL_GameControllerType type, SDL_GameControllerAxis axis) +{ + if (type == SDL_CONTROLLER_TYPE_PS3 || type == SDL_CONTROLLER_TYPE_PS4 || type == SDL_CONTROLLER_TYPE_PS5) + { + switch (axis) + { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return "L2"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return "R2"; + default: break; + } + } + else if (type == SDL_CONTROLLER_TYPE_XBOX360 || type == SDL_CONTROLLER_TYPE_XBOXONE) + { + switch (axis) + { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return "LT"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return "RT"; + default: break; + } + } + switch (axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: return "Left Stick X"; + case SDL_CONTROLLER_AXIS_LEFTY: return "Left Stick Y"; + case SDL_CONTROLLER_AXIS_RIGHTX: return "Right Stick X"; + case SDL_CONTROLLER_AXIS_RIGHTY: return "Right Stick Y"; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return "Left Trigger"; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return "Right Trigger"; + default: return NULL; + } +} + +int GetGameControllerMapping(GameController *restrict pad) +{ + const char *const btnmap_table[SDL_CONTROLLER_BUTTON_MAX] = { + [SDL_CONTROLLER_BUTTON_A] = "a", + [SDL_CONTROLLER_BUTTON_B] = "b", + [SDL_CONTROLLER_BUTTON_X] = "x", + [SDL_CONTROLLER_BUTTON_Y] = "y", + [SDL_CONTROLLER_BUTTON_BACK] = "back", + [SDL_CONTROLLER_BUTTON_GUIDE] = "guide", + [SDL_CONTROLLER_BUTTON_START] = "start", + [SDL_CONTROLLER_BUTTON_LEFTSTICK] = "leftstick", + [SDL_CONTROLLER_BUTTON_RIGHTSTICK] = "rightstick", + [SDL_CONTROLLER_BUTTON_LEFTSHOULDER] = "leftshoulder", + [SDL_CONTROLLER_BUTTON_RIGHTSHOULDER] = "rightshoulder", + [SDL_CONTROLLER_BUTTON_DPAD_UP] = "dpup", + [SDL_CONTROLLER_BUTTON_DPAD_DOWN] = "dpdown", + [SDL_CONTROLLER_BUTTON_DPAD_LEFT] = "dpleft", + [SDL_CONTROLLER_BUTTON_DPAD_RIGHT] = "dpright", + [SDL_CONTROLLER_BUTTON_MISC1] = "misc1", + [SDL_CONTROLLER_BUTTON_PADDLE1] = "paddle1", + [SDL_CONTROLLER_BUTTON_PADDLE2] = "paddle2", + [SDL_CONTROLLER_BUTTON_PADDLE3] = "paddle3", + [SDL_CONTROLLER_BUTTON_PADDLE4] = "paddle4", + [SDL_CONTROLLER_BUTTON_TOUCHPAD] = "touchpad" + }; + const char *const axismap_table[SDL_CONTROLLER_AXIS_MAX] = { + [SDL_CONTROLLER_AXIS_LEFTX] = "leftx", + [SDL_CONTROLLER_AXIS_LEFTY] = "lefty", + [SDL_CONTROLLER_AXIS_RIGHTX] = "rightx", + [SDL_CONTROLLER_AXIS_RIGHTY] = "righty", + [SDL_CONTROLLER_AXIS_TRIGGERLEFT] = "lefttrigger", + [SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = "righttrigger" + }; + + char *mapping = SDL_GameControllerMapping(pad->sdlpad); + if (!mapping) + return 1; + + gchar **items = g_strsplit(mapping, ",", 0); + const unsigned numitems = g_strv_length(items); + for (unsigned i = 2; i < numitems; ++i) + { + gchar **map = g_strsplit(items[i], ":", 2); + if (g_strv_length(items) < 2) + continue; + long joyval; + if (map[1][0] == 'b') + { + for (int j = 0; j < SDL_CONTROLLER_BUTTON_MAX; ++j) + if (g_strcmp0(map[0], btnmap_table[j]) == 0 && !IntFromStr(&map[1][1], &joyval)) + pad->map_button[j] = joyval; + } + if (map[1][0] == 'a') + { + for (int j = 0; j < SDL_CONTROLLER_AXIS_MAX; ++j) + if (g_strcmp0(map[0], axismap_table[j]) == 0 && !IntFromStr(&map[1][1], &joyval)) + pad->map_axis[j] = joyval; + } + g_strfreev(map); + } + + g_strfreev(items); + SDL_free(mapping); + return 0; +} + +#endif