From e74d907462b5986fb67a5343492130e4cca0d497 Mon Sep 17 00:00:00 2001 From: zoldalma999 <46655437+zoldalma999@users.noreply.github.com> Date: Sun, 26 Mar 2023 12:07:26 +0200 Subject: [PATCH 1/8] Port the controller module to C --- buildconfig/Setup.Emscripten.SDL2.in | 2 + buildconfig/Setup.SDL2.in | 1 + buildconfig/stubs/mypy_allow_list.txt | 4 - buildconfig/stubs/pygame/_sdl2/controller.pyi | 31 + docs/reST/ref/sdl2_controller.rst | 8 + examples/eventlist.py | 4 - src_c/_sdl2/controller.c | 585 ++++++++++++++++++ src_c/doc/sdl2_controller_doc.h | 1 + src_c/event.c | 3 +- src_c/static.c | 6 + src_py/_sdl2/controller.py | 1 - test/controller_test.py | 86 +-- 12 files changed, 637 insertions(+), 95 deletions(-) create mode 100644 buildconfig/stubs/pygame/_sdl2/controller.pyi create mode 100644 src_c/_sdl2/controller.c delete mode 100644 src_py/_sdl2/controller.py diff --git a/buildconfig/Setup.Emscripten.SDL2.in b/buildconfig/Setup.Emscripten.SDL2.in index 99fc298805..7c9fb58249 100644 --- a/buildconfig/Setup.Emscripten.SDL2.in +++ b/buildconfig/Setup.Emscripten.SDL2.in @@ -42,6 +42,7 @@ image src_c/void.c base src_c/void.c bufferproxy src_c/void.c color src_c/void.c +controller src_c/void.c controller_old src_c/void.c display src_c/void.c draw src_c/void.c @@ -63,6 +64,7 @@ rwobject src_c/void.c system src_c/void.c #_sdl2.controller src_c/_sdl2/controller.c $(SDL) $(DEBUG) -Isrc_c +_sdl2.controller src_c/void.c _sdl2.controller_old src_c/void.c #_sdl2.touch src_c/_sdl2/touch.c $(SDL) $(DEBUG) -Isrc_c diff --git a/buildconfig/Setup.SDL2.in b/buildconfig/Setup.SDL2.in index b3ae9e43e5..31d10a8f07 100644 --- a/buildconfig/Setup.SDL2.in +++ b/buildconfig/Setup.SDL2.in @@ -37,6 +37,7 @@ _sdl2.audio src_c/_sdl2/audio.c $(SDL) $(DEBUG) -Isrc_c _sdl2.video src_c/_sdl2/video.c src_c/pgcompat.c $(SDL) $(DEBUG) -Isrc_c _sdl2.mixer src_c/_sdl2/mixer.c $(SDL) $(MIXER) $(DEBUG) -Isrc_c _sdl2.touch src_c/_sdl2/touch.c $(SDL) $(DEBUG) -Isrc_c +_sdl2.controller src_c/_sdl2/controller.c $(SDL) $(DEBUG) -Isrc_c _sdl2.controller_old src_c/_sdl2/controller_old.c $(SDL) $(DEBUG) -Isrc_c GFX = src_c/SDL_gfx/SDL_gfxPrimitives.c diff --git a/buildconfig/stubs/mypy_allow_list.txt b/buildconfig/stubs/mypy_allow_list.txt index ef1dfb66cb..648b0f9143 100644 --- a/buildconfig/stubs/mypy_allow_list.txt +++ b/buildconfig/stubs/mypy_allow_list.txt @@ -29,7 +29,3 @@ pygame\.pkgdata pygame\.pypm pygame\._sdl2\.mixer pygame\.sysfont.* - -# temporarily ignore the controller module while it is being converted -# to C -pygame\._sdl2\.controller diff --git a/buildconfig/stubs/pygame/_sdl2/controller.pyi b/buildconfig/stubs/pygame/_sdl2/controller.pyi new file mode 100644 index 0000000000..89d64e7245 --- /dev/null +++ b/buildconfig/stubs/pygame/_sdl2/controller.pyi @@ -0,0 +1,31 @@ +from typing import Mapping +from pygame.joystick import JoystickType + +def init() -> None: ... +def get_init() -> bool: ... +def quit() -> None: ... +def is_controller(device_index: int) -> bool: ... +def get_count() -> int: ... + +class Controller: + def __new__(cls, device_index: int) -> Controller: ... + def __init__(self) -> None: ... + @classmethod + def from_joystick(cls, joystick: JoystickType) -> Controller: ... + def get_init(self) -> bool: ... + def init(self) -> None: ... + def quit(self) -> None: ... + @property + def id(self) -> int: ... + @property + def name(self) -> str: ... + def attached(self) -> bool: ... + def as_joystick(self) -> JoystickType: ... + def get_axis(self, axis: int) -> float: ... + def get_button(self, button: int) -> bool: ... + def get_mapping(self) -> dict: ... + def set_mapping(self, mapping: dict) -> int: ... + def rumble(self, ___) -> bool: ... + def stop_rumble( + self, low_frequency: float, high_frequency: float, duration: int + ) -> bool: ... diff --git a/docs/reST/ref/sdl2_controller.rst b/docs/reST/ref/sdl2_controller.rst index 62b16c3799..9190c3ac67 100644 --- a/docs/reST/ref/sdl2_controller.rst +++ b/docs/reST/ref/sdl2_controller.rst @@ -127,6 +127,14 @@ events related to controllers. ``pygame._sdl2.controller.from_joystick``. Controllers are initialized on creation. + .. method:: init + + | :sl:`Initialize the Controller` + | :sg:`init() -> None` + + Initialize a controller object. This should not be used much, since + Controllers are initialised on creation. + .. method:: quit | :sl:`uninitialize the Controller` diff --git a/examples/eventlist.py b/examples/eventlist.py index 44114462dd..c8db0c9f35 100644 --- a/examples/eventlist.py +++ b/examples/eventlist.py @@ -24,7 +24,6 @@ - press keys up an down to see events. - you can see joystick events if any are plugged in. -- press "c" to toggle events generated by controllers. """ import pygame as pg @@ -160,9 +159,6 @@ def main(): last_key = e.key if e.key == pg.K_h: draw_usage_in_history(history, usage) - if SDL2 and e.key == pg.K_c: - current_state = pg._sdl2.controller.get_eventstate() - pg._sdl2.controller.set_eventstate(not current_state) if e.type == pg.MOUSEBUTTONDOWN and e.button == 1: pg.event.set_grab(not pg.event.get_grab()) diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c new file mode 100644 index 0000000000..5198af1dde --- /dev/null +++ b/src_c/_sdl2/controller.c @@ -0,0 +1,585 @@ + +#include "../pgcompat.h" +#include "../pygame.h" +#include "structmember.h" + +#include "../doc/sdl2_controller_doc.h" +#define CONTROLLER_INIT_CHECK() \ + if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER)) \ + return RAISE(pgExc_SDLError, "Controller system not initialized"); + +typedef struct pgControllerObject { + PyObject_HEAD int id; + char *name; + SDL_GameController *controller; + + struct pgControllerObject *next; +} pgControllerObject; + +static pgControllerObject *_first_controller = NULL; + +static PyObject * +init(PyObject *module, PyObject *_null) +{ + if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + } + SDL_GameControllerEventState(SDL_ENABLE); + + Py_RETURN_NONE; +} + +static PyObject * +quit(PyObject *module, PyObject *_null) +{ + pgControllerObject *cur = _first_controller; + + while (cur) { + if (cur->controller) { + SDL_GameControllerClose(cur->controller); + cur->controller = NULL; + } + cur = cur->next; + } + + if (SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { + SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); + } + Py_RETURN_NONE; +} + +static void +_auto_quit(void) +{ + quit(NULL, NULL); +} + +static PyObject * +get_init(PyObject *module, PyObject *_null) +{ + return PyBool_FromLong(SDL_WasInit(SDL_INIT_GAMECONTROLLER) != 0); +} + +static PyObject * +is_controller(PyObject *module, PyObject *args, PyObject *kwargs) +{ + int device_index; + static char *keywords[] = {"device_index", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i", keywords, + &device_index)) { + return NULL; + } + + CONTROLLER_INIT_CHECK(); + + return PyBool_FromLong(SDL_IsGameController(device_index)); +} + +static PyObject * +get_count(PyObject *module, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + + int count = SDL_NumJoysticks(); + if (count < 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromLong(count); +} + +static PyMethodDef _controller_module_methods[] = { + {"init", (PyCFunction)init, METH_NOARGS, DOC_SDL2_CONTROLLER_INIT}, + {"quit", (PyCFunction)quit, METH_NOARGS, DOC_SDL2_CONTROLLER_QUIT}, + {"get_init", (PyCFunction)get_init, METH_NOARGS, + DOC_SDL2_CONTROLLER_GETINIT}, + {"is_controller", (PyCFunction)is_controller, METH_VARARGS | METH_KEYWORDS, + DOC_SDL2_CONTROLLER_ISCONTROLLER}, + {"get_count", (PyCFunction)get_count, METH_NOARGS, + DOC_SDL2_CONTROLLER_GETCOUNT}, + {NULL, NULL, 0, NULL}}; + +static PyObject * +controller_from_joystick(PyTypeObject *subtype, PyObject *args, + PyObject *kwargs) +{ + pgJoystickObject *joy; + static char *keywords[] = {"joystick", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", keywords, + &pgJoystick_Type, &joy)) { + return NULL; + } + + CONTROLLER_INIT_CHECK(); + return PyObject_CallFunction((PyObject *)subtype, "i", joy->id); +} + +static int +controller_init(pgControllerObject *, PyObject *, PyObject *); + +static PyObject * +controller_init_func(pgControllerObject *self) +{ + if (controller_init(self, NULL, NULL) == -1) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +controller_get_init(pgControllerObject *self, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + return PyBool_FromLong(self->controller != NULL); +} + +static PyObject * +controller_quit(pgControllerObject *self, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + if (self->controller) { + SDL_GameControllerClose(self->controller); + self->controller = NULL; + } + Py_RETURN_NONE; +} + +static PyObject * +controller_attached(pgControllerObject *self, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + return PyBool_FromLong(SDL_GameControllerGetAttached(self->controller)); +} + +static PyObject * +controller_as_joystick(pgControllerObject *self, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + JOYSTICK_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + return pgJoystick_New(self->id); +} + +static PyObject * +controller_get_axis(pgControllerObject *self, PyObject *args, PyObject *kwargs) +{ + int axis; + static char *keywords[] = {"axis", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i", keywords, &axis)) { + return NULL; + } + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + if (axis < 0 || axis > SDL_CONTROLLER_AXIS_MAX - 1) { + return RAISE(pgExc_SDLError, "Invalid axis"); + } + + return PyFloat_FromDouble( + SDL_GameControllerGetAxis(self->controller, axis) / 32767.0); +} + +static PyObject * +controller_get_button(pgControllerObject *self, PyObject *args, + PyObject *kwargs) +{ + int button; + static char *keywords[] = {"button", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i", keywords, &button)) { + return NULL; + } + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + if (button < 0 || button > SDL_CONTROLLER_BUTTON_MAX) { + return RAISE(pgExc_SDLError, "Invalid button"); + } + + return PyBool_FromLong( + SDL_GameControllerGetButton(self->controller, button)); +} + +static PyObject * +controller_get_mapping(pgControllerObject *self, PyObject *_null) +{ + char *mapping, *key, *value; + char *token, *saveptr = NULL; + PyObject *dict, *value_obj = NULL; + + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + mapping = SDL_GameControllerMapping(self->controller); + if (!mapping) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + dict = PyDict_New(); + if (!dict) { + goto err; + } + + token = SDL_strtokr(mapping, ",", &saveptr); + while (token != NULL) { + key = SDL_strtokr(token, ":", &value); + + if (value[0] != '\0') { + value_obj = PyUnicode_FromString(value); + if (!value_obj) { + goto err; + } + if (PyDict_SetItemString(dict, key, value_obj)) { + goto err; + } + Py_DECREF(value_obj); + } + token = SDL_strtokr(NULL, ",", &saveptr); + } + + SDL_free(saveptr); + SDL_free(mapping); + return dict; + +err: + Py_XDECREF(value_obj); + Py_XDECREF(dict); + SDL_free(mapping); + SDL_free(saveptr); + return NULL; +} + +static PyObject * +controller_set_mapping(pgControllerObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *dict; + static char *keywords[] = {"mapping", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", keywords, + &PyDict_Type, &dict)) { + return NULL; + } + + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + + char guid_str[64]; + SDL_Joystick *joy = SDL_GameControllerGetJoystick(self->controller); + SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(joy), guid_str, 63); + + PyObject *key, *value; + const char *key_str, *value_str; + Py_ssize_t dict_index = 0; + int offset = 0, size = 512; + char *mapping = malloc(size * sizeof(char)); + if (mapping == NULL) { + return PyErr_NoMemory(); + } + + while (PyDict_Next(dict, &dict_index, &key, &value)) { + if (!PyUnicode_Check(key) || !PyUnicode_Check(value)) { + free(mapping); + return RAISE(PyExc_TypeError, "Dict items must be strings"); + } + + key_str = PyUnicode_AsUTF8(key); + value_str = PyUnicode_AsUTF8(value); + if (key_str == NULL || value_str == NULL) { + free(mapping); + return NULL; + } + + int res = SDL_snprintf(mapping + offset, size - offset, ",%s:%s", + key_str, value_str); + if (res < 0) { + free(mapping); + return RAISE(PyExc_RuntimeError, "Internal snprintf call failed"); + } + else if (res >= size - offset) { + // Retry the same key value pair with more memory allocated + dict_index -= 1; + size *= 2; + mapping = realloc(mapping, size); + if (mapping == NULL) { + return PyErr_NoMemory(); + } + continue; + } + offset += res; + } + + int res_size = offset + 64 + (int)SDL_strlen(self->name); + char *mapping_string = malloc(res_size * sizeof(char)); + SDL_snprintf(mapping_string, res_size, "%s,%s%s", guid_str, self->name, + mapping); + + int res = SDL_GameControllerAddMapping(mapping_string); + free(mapping); + free(mapping_string); + + if (res < 0) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + return PyLong_FromLong(res); +} + +static PyObject * +controller_rumble(pgControllerObject *self, PyObject *args, PyObject *kwargs) +{ + double low_freq, high_freq; + Uint32 duration; + static char *keywords[] = {"low_frequency", "high_frequency", "duration", + NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ddI", keywords, &low_freq, + &high_freq, &duration)) { + return NULL; + } + + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } + +#if SDL_VERSION_ATLEAST(2, 0, 9) + low_freq = MAX(MIN(low_freq, 1.0f), 0.0f) * 65535; + high_freq = MAX(MIN(high_freq, 1.0f), 0.0f) * 65535; + + int success = SDL_GameControllerRumble(self->controller, (Uint16)low_freq, + (Uint16)high_freq, duration); + + return PyBool_FromLong(success == 0); +#else + Py_RETURN_FALSE; +#endif +} + +static PyObject * +controller_stop_rumble(pgControllerObject *self, PyObject *_null) +{ + CONTROLLER_INIT_CHECK(); + if (!self->controller) { + return RAISE(pgExc_SDLError, "Controller is not initalized"); + } +#if SDL_VERSION_ATLEAST(2, 0, 9) + SDL_GameControllerRumble(self->controller, 0, 0, 1); +#endif + Py_RETURN_NONE; +} + +static PyMethodDef controller_methods[] = { + {"from_joystick", (PyCFunction)controller_from_joystick, + METH_CLASS | METH_VARARGS | METH_KEYWORDS, + DOC_SDL2_CONTROLLER_CONTROLLER_FROMJOYSTICK}, + {"get_init", (PyCFunction)controller_get_init, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_GETINIT}, + {"init", (PyCFunction)controller_init_func, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_INIT}, + {"quit", (PyCFunction)controller_quit, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_QUIT}, + {"attached", (PyCFunction)controller_attached, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_ATTACHED}, + {"as_joystick", (PyCFunction)controller_as_joystick, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_ASJOYSTICK}, + {"get_axis", (PyCFunction)controller_get_axis, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_CONTROLLER_CONTROLLER_ASJOYSTICK}, + {"get_button", (PyCFunction)controller_get_button, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_CONTROLLER_CONTROLLER_GETBUTTON}, + {"get_mapping", (PyCFunction)controller_get_mapping, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_GETMAPPING}, + {"set_mapping", (PyCFunction)controller_set_mapping, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_CONTROLLER_CONTROLLER_SETMAPPING}, + {"rumble", (PyCFunction)controller_rumble, METH_VARARGS | METH_KEYWORDS, + DOC_SDL2_CONTROLLER_CONTROLLER_RUMBLE}, + {"stop_rumble", (PyCFunction)controller_stop_rumble, METH_NOARGS, + DOC_SDL2_CONTROLLER_CONTROLLER_STOPRUMBLE}, + {NULL, NULL, 0, NULL}}; + +static PyMemberDef controller_members[] = { + {"id", T_INT, offsetof(pgControllerObject, id), READONLY, + "Gets the id of the controller"}, + {"name", T_STRING, offsetof(pgControllerObject, name), READONLY, + "Gets the name of the controller"}, + {NULL}, +}; + +static PyObject * +controller_new(PyTypeObject *subtype, PyObject *args, PyObject *kwargs) +{ + int id; + pgControllerObject *self, *cur; + static char *keywords[] = {"device_index", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i", keywords, &id)) { + return NULL; + } + + CONTROLLER_INIT_CHECK(); + + if (id >= SDL_NumJoysticks() || !SDL_IsGameController(id)) { + return RAISE(pgExc_SDLError, "Invalid index"); + } + + SDL_GameController *controller = SDL_GameControllerOpen(id); + if (!controller) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + + cur = _first_controller; + while (cur) { + if (cur->controller == controller) { + Py_INCREF(cur); + return (PyObject *)cur; + } + + if (!cur->next) { + break; + } + + cur = cur->next; + } + self = PyObject_New(pgControllerObject, subtype); + if (!self) { + return NULL; + } + + if (!_first_controller) { + _first_controller = self; + } + else { + cur->next = self; + } + self->next = NULL; + self->controller = controller; + self->id = id; + self->name = NULL; + return (PyObject *)self; +} + +static int +controller_init(pgControllerObject *self, PyObject *args, PyObject *kwargs) +{ + SDL_GameController *controller; + + if (self->controller == NULL) { + controller = SDL_GameControllerOpen(self->id); + if (controller == NULL) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + self->controller = controller; + } + + if (self->name) { + free(self->name); + } + self->name = strdup(SDL_GameControllerName(self->controller)); + + return 0; +} + +void +controller_dealloc(pgControllerObject *self) +{ + pgControllerObject *cur, *prev; + if (self->controller && SDL_GameControllerGetAttached(self->controller)) { + SDL_GameControllerClose(self->controller); + } + self->controller = NULL; + free(self->name); + + cur = _first_controller; + prev = NULL; + while (cur) { + if (cur == self) { + if (!prev) { + _first_controller = self->next; + break; + } + prev->next = self->next; + break; + } + + prev = cur; + cur = cur->next; + } + PyObject_DEL(self); +} + +static PyTypeObject pgController_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "Controller", + .tp_basicsize = sizeof(pgControllerObject), + .tp_doc = DOC_SDL2_CONTROLLER_CONTROLLER, + .tp_new = controller_new, + .tp_init = (initproc)controller_init, + .tp_dealloc = (destructor)controller_dealloc, + .tp_methods = controller_methods, + .tp_members = controller_members, +}; + +MODINIT_DEFINE(controller) +{ + PyObject *module; + + static struct PyModuleDef _module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "controller", + .m_doc = DOC_SDL2_CONTROLLER, + .m_size = -1, + .m_methods = _controller_module_methods}; + + import_pygame_base(); + if (PyErr_Occurred()) { + return NULL; + } + + import_pygame_joystick(); + if (PyErr_Occurred()) { + return NULL; + } + + module = PyModule_Create(&_module); + + if (!module) { + return NULL; + } + + if (PyType_Ready(&pgController_Type) < 0) { + return NULL; + } + + Py_INCREF(&pgController_Type); + if (PyModule_AddObject(module, "Controller", + (PyObject *)&pgController_Type)) { + Py_DECREF(&pgController_Type); + Py_DECREF(module); + return NULL; + } + + /* note: whenever this module gets released from _sdl2, base.c _pg_quit + * should should quit the controller module automatically, instead of + * doing this */ + pg_RegisterQuit(_auto_quit); + + return module; +} diff --git a/src_c/doc/sdl2_controller_doc.h b/src_c/doc/sdl2_controller_doc.h index a7cda71f62..6ca397a45d 100644 --- a/src_c/doc/sdl2_controller_doc.h +++ b/src_c/doc/sdl2_controller_doc.h @@ -9,6 +9,7 @@ #define DOC_SDL2_CONTROLLER_ISCONTROLLER "is_controller(index) -> bool\nCheck if the given joystick is supported by the game controller interface" #define DOC_SDL2_CONTROLLER_NAMEFORINDEX "name_forindex(index) -> name or None\nGet the name of the controller" #define DOC_SDL2_CONTROLLER_CONTROLLER "Controller(index) -> Controller\nCreate a new Controller object." +#define DOC_SDL2_CONTROLLER_CONTROLLER_INIT "init() -> None\nInitialize the Controller" #define DOC_SDL2_CONTROLLER_CONTROLLER_QUIT "quit() -> None\nuninitialize the Controller" #define DOC_SDL2_CONTROLLER_CONTROLLER_GETINIT "get_init() -> bool\ncheck if the Controller is initialized" #define DOC_SDL2_CONTROLLER_CONTROLLER_FROMJOYSTICK "from_joystick(joystick) -> Controller\nCreate a Controller from a pygame.joystick.Joystick object" diff --git a/src_c/event.c b/src_c/event.c index 6d5a45dac3..d32f8a9666 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -1143,7 +1143,8 @@ dict_from_event(SDL_Event *event) _pg_insobj(dict, "instance_id", PyLong_FromLong(event->caxis.which)); _pg_insobj(dict, "axis", PyLong_FromLong(event->caxis.axis)); - _pg_insobj(dict, "value", PyLong_FromLong(event->caxis.value)); + _pg_insobj(dict, "value", + PyFloat_FromDouble(event->caxis.value / 32767.0)); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: diff --git a/src_c/static.c b/src_c/static.c index 9b813457bf..e9737d8106 100644 --- a/src_c/static.c +++ b/src_c/static.c @@ -134,6 +134,9 @@ PyInit_mixer(void); PyMODINIT_FUNC PyInit_system(void); +PyMODINIT_FUNC +PyInit_controller(void); + PyMODINIT_FUNC PyInit_controller_old(void); @@ -228,6 +231,8 @@ mod_pygame_import_cython(PyObject *self, PyObject *spec) load_submodule_mphase("pygame._sdl2", PyInit_mixer(), spec, "mixer"); load_submodule_mphase("pygame._sdl2", PyInit_controller_old(), spec, "controller_old"); + load_submodule_mphase("pygame._sdl2", PyInit_controller(), spec, + "controller"); load_submodule_mphase("pygame._sdl2", PyInit_audio(), spec, "audio"); load_submodule_mphase("pygame._sdl2", PyInit_video(), spec, "video"); // depends on pygame._sdl2.video @@ -389,6 +394,7 @@ PyInit_pygame_static() #include "pixelcopy.c" #include "newbuffer.c" +#include "_sdl2/controller.c" #include "_sdl2/controller_old.c" #include "_sdl2/touch.c" #include "transform.c" diff --git a/src_py/_sdl2/controller.py b/src_py/_sdl2/controller.py deleted file mode 100644 index 676f9fde27..0000000000 --- a/src_py/_sdl2/controller.py +++ /dev/null @@ -1 +0,0 @@ -from .controller_old import * # pylint: disable=wildcard-import diff --git a/test/controller_test.py b/test/controller_test.py index 4b992f113e..e2f767272b 100644 --- a/test/controller_test.py +++ b/test/controller_test.py @@ -4,7 +4,6 @@ from pygame.tests.test_utils import prompt, question -@unittest.skip("Module is under construction") class ControllerModuleTest(unittest.TestCase): def setUp(self): controller.init() @@ -34,15 +33,6 @@ def test_quit__multiple(self): def test_get_init(self): self.assertTrue(controller.get_init()) - def test_get_eventstate(self): - controller.set_eventstate(True) - self.assertTrue(controller.get_eventstate()) - - controller.set_eventstate(False) - self.assertFalse(controller.get_eventstate()) - - controller.set_eventstate(True) - def test_get_count(self): self.assertGreaterEqual(controller.get_count(), 0) @@ -59,11 +49,7 @@ def test_is_controller(self): with self.assertRaises(TypeError): controller.is_controller("Test") - def test_name_forindex(self): - self.assertIsNone(controller.name_forindex(-1)) - -@unittest.skip("Module is under construction") class ControllerTypeTest(unittest.TestCase): def setUp(self): controller.init() @@ -149,7 +135,6 @@ def test_set_mapping(self): self.skipTest("No controller connected") -@unittest.skip("Module is under construction") class ControllerInteractiveTest(unittest.TestCase): __tags__ = ["interactive"] @@ -182,75 +167,6 @@ def test__get_count_interactive(self): self.assertTrue(ans) - def test_set_eventstate_on_interactive(self): - c = self._get_first_controller() - if not c: - self.skipTest("No controller connected") - - pygame.display.init() - pygame.font.init() - - screen = pygame.display.set_mode((400, 400)) - font = pygame.font.Font(None, 20) - running = True - - screen.fill((255, 255, 255)) - screen.blit( - font.render("Press button 'x' (on ps4) or 'a' (on xbox).", True, (0, 0, 0)), - (0, 0), - ) - pygame.display.update() - - controller.set_eventstate(True) - - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - - if event.type == pygame.CONTROLLERBUTTONDOWN: - running = False - - pygame.display.quit() - pygame.font.quit() - - def test_set_eventstate_off_interactive(self): - c = self._get_first_controller() - if not c: - self.skipTest("No controller connected") - - pygame.display.init() - pygame.font.init() - - screen = pygame.display.set_mode((400, 400)) - font = pygame.font.Font(None, 20) - running = True - - screen.fill((255, 255, 255)) - screen.blit( - font.render("Press button 'x' (on ps4) or 'a' (on xbox).", True, (0, 0, 0)), - (0, 0), - ) - pygame.display.update() - - controller.set_eventstate(False) - - while running: - for event in pygame.event.get(pygame.QUIT): - if event: - running = False - - if c.get_button(pygame.CONTROLLER_BUTTON_A): - if pygame.event.peek(pygame.CONTROLLERBUTTONDOWN): - pygame.display.quit() - pygame.font.quit() - self.fail() - else: - running = False - - pygame.display.quit() - pygame.font.quit() - def test_get_button_interactive(self): c = self._get_first_controller() if not c: @@ -326,7 +242,7 @@ def test_get_axis_interactive(self): ) label3 = font.render( - 'be in the range of 0-32767. Press "y" or "n" to confirm.', True, (0, 0, 0) + 'be in the range of 0-1. Press "y" or "n" to confirm.', True, (0, 0, 0) ) while running: From 40b9033250c971c74f1f6bfee8aeea695aaa30f0 Mon Sep 17 00:00:00 2001 From: zoldalma999 <46655437+zoldalma999@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:57:22 +0200 Subject: [PATCH 2/8] Fix CI --- buildconfig/stubs/pygame/_sdl2/controller.pyi | 6 +-- src_c/_sdl2/controller.c | 39 +++++++++++-------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/buildconfig/stubs/pygame/_sdl2/controller.pyi b/buildconfig/stubs/pygame/_sdl2/controller.pyi index 89d64e7245..bce688d595 100644 --- a/buildconfig/stubs/pygame/_sdl2/controller.pyi +++ b/buildconfig/stubs/pygame/_sdl2/controller.pyi @@ -1,4 +1,4 @@ -from typing import Mapping +from typing import Mapping, final from pygame.joystick import JoystickType def init() -> None: ... @@ -6,10 +6,10 @@ def get_init() -> bool: ... def quit() -> None: ... def is_controller(device_index: int) -> bool: ... def get_count() -> int: ... - +@final class Controller: def __new__(cls, device_index: int) -> Controller: ... - def __init__(self) -> None: ... + def __init__(self, device_index: int) -> None: ... @classmethod def from_joystick(cls, joystick: JoystickType) -> Controller: ... def get_init(self) -> bool: ... diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c index 5198af1dde..caffded041 100644 --- a/src_c/_sdl2/controller.c +++ b/src_c/_sdl2/controller.c @@ -18,8 +18,12 @@ typedef struct pgControllerObject { static pgControllerObject *_first_controller = NULL; +#if defined(_MSC_VER) +#define strtok_r strtok_s +#endif + static PyObject * -init(PyObject *module, PyObject *_null) +controller_module_init(PyObject *module, PyObject *_null) { if (!SDL_WasInit(SDL_INIT_GAMECONTROLLER)) { if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER)) { @@ -32,7 +36,7 @@ init(PyObject *module, PyObject *_null) } static PyObject * -quit(PyObject *module, PyObject *_null) +controller_module_quit(PyObject *module, PyObject *_null) { pgControllerObject *cur = _first_controller; @@ -51,19 +55,20 @@ quit(PyObject *module, PyObject *_null) } static void -_auto_quit(void) +_controller_module_auto_quit(void) { - quit(NULL, NULL); + controller_module_quit(NULL, NULL); } static PyObject * -get_init(PyObject *module, PyObject *_null) +controller_module_get_init(PyObject *module, PyObject *_null) { return PyBool_FromLong(SDL_WasInit(SDL_INIT_GAMECONTROLLER) != 0); } static PyObject * -is_controller(PyObject *module, PyObject *args, PyObject *kwargs) +controller_module_is_controller(PyObject *module, PyObject *args, + PyObject *kwargs) { int device_index; static char *keywords[] = {"device_index", NULL}; @@ -78,7 +83,7 @@ is_controller(PyObject *module, PyObject *args, PyObject *kwargs) } static PyObject * -get_count(PyObject *module, PyObject *_null) +controller_module_get_count(PyObject *module, PyObject *_null) { CONTROLLER_INIT_CHECK(); @@ -91,13 +96,15 @@ get_count(PyObject *module, PyObject *_null) } static PyMethodDef _controller_module_methods[] = { - {"init", (PyCFunction)init, METH_NOARGS, DOC_SDL2_CONTROLLER_INIT}, - {"quit", (PyCFunction)quit, METH_NOARGS, DOC_SDL2_CONTROLLER_QUIT}, - {"get_init", (PyCFunction)get_init, METH_NOARGS, + {"init", (PyCFunction)controller_module_init, METH_NOARGS, + DOC_SDL2_CONTROLLER_INIT}, + {"quit", (PyCFunction)controller_module_quit, METH_NOARGS, + DOC_SDL2_CONTROLLER_QUIT}, + {"get_init", (PyCFunction)controller_module_get_init, METH_NOARGS, DOC_SDL2_CONTROLLER_GETINIT}, - {"is_controller", (PyCFunction)is_controller, METH_VARARGS | METH_KEYWORDS, - DOC_SDL2_CONTROLLER_ISCONTROLLER}, - {"get_count", (PyCFunction)get_count, METH_NOARGS, + {"is_controller", (PyCFunction)controller_module_is_controller, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_CONTROLLER_ISCONTROLLER}, + {"get_count", (PyCFunction)controller_module_get_count, METH_NOARGS, DOC_SDL2_CONTROLLER_GETCOUNT}, {NULL, NULL, 0, NULL}}; @@ -237,9 +244,9 @@ controller_get_mapping(pgControllerObject *self, PyObject *_null) goto err; } - token = SDL_strtokr(mapping, ",", &saveptr); + token = strtok_r(mapping, ",", &saveptr); while (token != NULL) { - key = SDL_strtokr(token, ":", &value); + key = strtok_r(token, ":", &value); if (value[0] != '\0') { value_obj = PyUnicode_FromString(value); @@ -579,7 +586,7 @@ MODINIT_DEFINE(controller) /* note: whenever this module gets released from _sdl2, base.c _pg_quit * should should quit the controller module automatically, instead of * doing this */ - pg_RegisterQuit(_auto_quit); + pg_RegisterQuit(_controller_module_auto_quit); return module; } From b8d927d98fb4c3b00d3fcbd526f96148a76de7b7 Mon Sep 17 00:00:00 2001 From: zoldalma999 <46655437+zoldalma999@users.noreply.github.com> Date: Sun, 14 May 2023 09:04:11 +0200 Subject: [PATCH 3/8] Fix ci part 2 --- buildconfig/stubs/pygame/_sdl2/controller.pyi | 2 +- src_c/_sdl2/controller.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildconfig/stubs/pygame/_sdl2/controller.pyi b/buildconfig/stubs/pygame/_sdl2/controller.pyi index bce688d595..fb95ea30a8 100644 --- a/buildconfig/stubs/pygame/_sdl2/controller.pyi +++ b/buildconfig/stubs/pygame/_sdl2/controller.pyi @@ -1,4 +1,4 @@ -from typing import Mapping, final +from typing import final from pygame.joystick import JoystickType def init() -> None: ... diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c index caffded041..6470ab57d7 100644 --- a/src_c/_sdl2/controller.c +++ b/src_c/_sdl2/controller.c @@ -258,7 +258,7 @@ controller_get_mapping(pgControllerObject *self, PyObject *_null) } Py_DECREF(value_obj); } - token = SDL_strtokr(NULL, ",", &saveptr); + token = strtok_r(NULL, ",", &saveptr); } SDL_free(saveptr); From 73b62735cf18f10b7953c69919546b0c2152e956 Mon Sep 17 00:00:00 2001 From: zoldalma <46655437+zoldalma999@users.noreply.github.com> Date: Sat, 3 Jun 2023 06:38:07 +0200 Subject: [PATCH 4/8] Add comments on axis and rumble ranges --- src_c/_sdl2/controller.c | 2 ++ src_c/event.c | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c index 6470ab57d7..c199f2eac8 100644 --- a/src_c/_sdl2/controller.c +++ b/src_c/_sdl2/controller.c @@ -195,6 +195,7 @@ controller_get_axis(pgControllerObject *self, PyObject *args, PyObject *kwargs) return RAISE(pgExc_SDLError, "Invalid axis"); } + // GetAxis returns values from -32768 to 32767 return PyFloat_FromDouble( SDL_GameControllerGetAxis(self->controller, axis) / 32767.0); } @@ -369,6 +370,7 @@ controller_rumble(pgControllerObject *self, PyObject *args, PyObject *kwargs) } #if SDL_VERSION_ATLEAST(2, 0, 9) + // rumble takes values in range 0 to 0xFFFF (65535) low_freq = MAX(MIN(low_freq, 1.0f), 0.0f) * 65535; high_freq = MAX(MIN(high_freq, 1.0f), 0.0f) * 65535; diff --git a/src_c/event.c b/src_c/event.c index 1173c73810..cb6e89bc7f 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -999,8 +999,9 @@ dict_from_event(SDL_Event *event) _pg_insobj(dict, "instance_id", PyLong_FromLong(event->jaxis.which)); _pg_insobj(dict, "axis", PyLong_FromLong(event->jaxis.axis)); + // sdl report axis values as values between -32768 and 32767 _pg_insobj(dict, "value", - PyFloat_FromDouble(event->jaxis.value / 32767.0)); + PyFloat_FromDouble(event->jaxis.value / 32768.0)); break; case SDL_JOYBALLMOTION: _pg_insobj(dict, "joy", _joy_map_instance(event->jaxis.which)); @@ -1143,8 +1144,9 @@ dict_from_event(SDL_Event *event) _pg_insobj(dict, "instance_id", PyLong_FromLong(event->caxis.which)); _pg_insobj(dict, "axis", PyLong_FromLong(event->caxis.axis)); + // sdl report axis values as values between -32768 and 32767 _pg_insobj(dict, "value", - PyFloat_FromDouble(event->caxis.value / 32767.0)); + PyFloat_FromDouble(event->caxis.value / 32768.0)); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: From 72ea8f0a19981dcef4ac1e66b394e9310779ffbc Mon Sep 17 00:00:00 2001 From: zoldalma <46655437+zoldalma999@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:22:44 +0200 Subject: [PATCH 5/8] Fix meson build of controller module --- src_c/_sdl2/meson.build | 9 +++++++++ src_py/_sdl2/meson.build | 14 +++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src_c/_sdl2/meson.build b/src_c/_sdl2/meson.build index 0478876c8c..2ad443e993 100644 --- a/src_c/_sdl2/meson.build +++ b/src_c/_sdl2/meson.build @@ -56,3 +56,12 @@ _sdl2_touch = py.extension_module( install: true, subdir: pg / '_sdl2', ) + +_sdl2_controller = py.extension_module( + 'controller', + 'controller.c', + dependencies: pg_base_deps, + include_directories: '..', + install: true, + subdir: pg / '_sdl2', +) diff --git a/src_py/_sdl2/meson.build b/src_py/_sdl2/meson.build index 261603fdcd..c3aabd3b4a 100644 --- a/src_py/_sdl2/meson.build +++ b/src_py/_sdl2/meson.build @@ -1,7 +1,7 @@ -# pure python sources -python_sources = files( - '__init__.py', - 'controller.py', - 'window.py', -) -py.install_sources(python_sources, subdir: pg / '_sdl2') +# # pure python sources +# python_sources = files( +# '__init__.py', +# 'controller.py', +# 'window.py', +# ) +# py.install_sources(python_sources, subdir: pg / '_sdl2') From 5cfbfaec8c586d7fc6b8c85c46b83cc6da23be6d Mon Sep 17 00:00:00 2001 From: zoldalma <46655437+zoldalma999@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:43:53 +0200 Subject: [PATCH 6/8] Revert change to get_axis return range --- buildconfig/stubs/pygame/_sdl2/controller.pyi | 2 +- src_c/_sdl2/controller.c | 4 +--- src_c/event.c | 4 +--- test/controller_test.py | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/buildconfig/stubs/pygame/_sdl2/controller.pyi b/buildconfig/stubs/pygame/_sdl2/controller.pyi index fb95ea30a8..1ff2ac9188 100644 --- a/buildconfig/stubs/pygame/_sdl2/controller.pyi +++ b/buildconfig/stubs/pygame/_sdl2/controller.pyi @@ -21,7 +21,7 @@ class Controller: def name(self) -> str: ... def attached(self) -> bool: ... def as_joystick(self) -> JoystickType: ... - def get_axis(self, axis: int) -> float: ... + def get_axis(self, axis: int) -> int: ... def get_button(self, button: int) -> bool: ... def get_mapping(self) -> dict: ... def set_mapping(self, mapping: dict) -> int: ... diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c index c199f2eac8..0792e95b2d 100644 --- a/src_c/_sdl2/controller.c +++ b/src_c/_sdl2/controller.c @@ -195,9 +195,7 @@ controller_get_axis(pgControllerObject *self, PyObject *args, PyObject *kwargs) return RAISE(pgExc_SDLError, "Invalid axis"); } - // GetAxis returns values from -32768 to 32767 - return PyFloat_FromDouble( - SDL_GameControllerGetAxis(self->controller, axis) / 32767.0); + return PyLong_FromLong(SDL_GameControllerGetAxis(self->controller, axis)); } static PyObject * diff --git a/src_c/event.c b/src_c/event.c index 09c09234ac..59132e3330 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -1186,9 +1186,7 @@ dict_from_event(SDL_Event *event) _pg_insobj(dict, "instance_id", PyLong_FromLong(event->caxis.which)); _pg_insobj(dict, "axis", PyLong_FromLong(event->caxis.axis)); - // sdl report axis values as values between -32768 and 32767 - _pg_insobj(dict, "value", - PyFloat_FromDouble(event->caxis.value / 32768.0)); + _pg_insobj(dict, "value", PyLong_FromLong(event->caxis.value)); break; case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: diff --git a/test/controller_test.py b/test/controller_test.py index ff8786de53..cdc36f8b78 100644 --- a/test/controller_test.py +++ b/test/controller_test.py @@ -236,7 +236,7 @@ def test_get_axis_interactive(self): ) label3 = font.render( - 'be in the range of 0-1. Press "y" or "n" to confirm.', True, (0, 0, 0) + 'be in the range of 0-32767. Press "y" or "n" to confirm.', True, (0, 0, 0) ) while running: From e412119d65f1e07659a47fff30ec3b5ac21a1d16 Mon Sep 17 00:00:00 2001 From: zoldalma <46655437+zoldalma999@users.noreply.github.com> Date: Tue, 27 Aug 2024 22:53:28 +0200 Subject: [PATCH 7/8] Fix get_mapping segfault, misc changes --- buildconfig/stubs/mypy_allow_list.txt | 3 --- src_c/_sdl2/controller.c | 2 +- test/controller_test.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/buildconfig/stubs/mypy_allow_list.txt b/buildconfig/stubs/mypy_allow_list.txt index 858eae32fc..d8f8c6c0d3 100644 --- a/buildconfig/stubs/mypy_allow_list.txt +++ b/buildconfig/stubs/mypy_allow_list.txt @@ -27,6 +27,3 @@ pygame\.pypm pygame\._sdl2\.mixer pygame\.sysfont.* pygame\.docs.* -# temporarily ignore the controller module while it is being converted -# to C -pygame\._sdl2\.controller diff --git a/src_c/_sdl2/controller.c b/src_c/_sdl2/controller.c index 0792e95b2d..bc62f90347 100644 --- a/src_c/_sdl2/controller.c +++ b/src_c/_sdl2/controller.c @@ -247,7 +247,7 @@ controller_get_mapping(pgControllerObject *self, PyObject *_null) while (token != NULL) { key = strtok_r(token, ":", &value); - if (value[0] != '\0') { + if (value != NULL && value[0] != '\0') { value_obj = PyUnicode_FromString(value); if (!value_obj) { goto err; diff --git a/test/controller_test.py b/test/controller_test.py index cdc36f8b78..000ee40a6d 100644 --- a/test/controller_test.py +++ b/test/controller_test.py @@ -43,7 +43,7 @@ def test_is_controller(self): self.assertIsInstance(c, controller.Controller) c.quit() else: - with self.assertRaises(pygame._sdl2.sdl2.error): + with self.assertRaises(pygame.error): c = controller.Controller(i) with self.assertRaises(TypeError): From 4eff7e1cb01c8e76664f269bbe7cb8309bfc8f2f Mon Sep 17 00:00:00 2001 From: zoldalma <46655437+zoldalma999@users.noreply.github.com> Date: Wed, 28 Aug 2024 09:32:10 +0200 Subject: [PATCH 8/8] Fix meson build --- src_py/_sdl2/meson.build | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src_py/_sdl2/meson.build b/src_py/_sdl2/meson.build index c3aabd3b4a..766b1161fe 100644 --- a/src_py/_sdl2/meson.build +++ b/src_py/_sdl2/meson.build @@ -1,7 +1,6 @@ -# # pure python sources -# python_sources = files( -# '__init__.py', -# 'controller.py', -# 'window.py', -# ) -# py.install_sources(python_sources, subdir: pg / '_sdl2') +# pure python sources +python_sources = files( + '__init__.py', + 'window.py', +) +py.install_sources(python_sources, subdir: pg / '_sdl2')