diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index b6f5eb3..ab86bab 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -124,16 +124,18 @@ void GameEngine::cleanup() McRFPy_API::game = nullptr; } - // Shutdown ImGui before closing window + // Close window FIRST - ImGui-SFML requires window to be closed before Shutdown() + // because Shutdown() destroys sf::Cursor objects that the window may reference. + // See: modules/imgui-sfml/README.md - "Call ImGui::SFML::Shutdown() after window.close()" + if (window && window->isOpen()) { + window->close(); + } + + // Shutdown ImGui AFTER window is closed to avoid X11 BadCursor errors if (imguiInitialized) { ImGui::SFML::Shutdown(); imguiInitialized = false; } - - // Force close the window if it's still open - if (window && window->isOpen()) { - window->close(); - } } Scene* GameEngine::currentScene() { return scenes[scene]; } diff --git a/src/GridLayers.cpp b/src/GridLayers.cpp index 4cc7af4..16f02c4 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -4,6 +4,7 @@ #include "PyColor.h" #include "PyTexture.h" #include "PyFOV.h" +#include "PyPositionHelper.h" #include // ============================================================================= @@ -562,10 +563,18 @@ void TileLayer::render(sf::RenderTarget& target, // ============================================================================= PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = { - {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS, - "at(x, y) -> Color\n\nGet the color at cell position (x, y)."}, + {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS | METH_KEYWORDS, + "at(pos) -> Color\nat(x, y) -> Color\n\n" + "Get the color at cell position.\n\n" + "Args:\n" + " pos: Position as (x, y) tuple, list, or Vector\n" + " x, y: Position as separate integer arguments"}, {"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS, - "set(x, y, color)\n\nSet the color at cell position (x, y)."}, + "set(pos, color)\n\n" + "Set the color at cell position.\n\n" + "Args:\n" + " pos: Position as (x, y) tuple, list, or Vector\n" + " color: Color object or (r, g, b[, a]) tuple"}, {"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS, "fill(color)\n\nFill the entire layer with the specified color."}, {"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, @@ -646,9 +655,9 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py return 0; } -PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) { +PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { return NULL; } @@ -678,9 +687,14 @@ PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args } PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) { - int x, y; + PyObject* pos_obj; PyObject* color_obj; - if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) { + if (!PyArg_ParseTuple(args, "OO", &pos_obj, &color_obj)) { + return NULL; + } + + int x, y; + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { return NULL; } @@ -1108,10 +1122,18 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) { // ============================================================================= PyMethodDef PyGridLayerAPI::TileLayer_methods[] = { - {"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS, - "at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."}, + {"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS | METH_KEYWORDS, + "at(pos) -> int\nat(x, y) -> int\n\n" + "Get the tile index at cell position. Returns -1 if no tile.\n\n" + "Args:\n" + " pos: Position as (x, y) tuple, list, or Vector\n" + " x, y: Position as separate integer arguments"}, {"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS, - "set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."}, + "set(pos, index)\n\n" + "Set the tile index at cell position. Use -1 for no tile.\n\n" + "Args:\n" + " pos: Position as (x, y) tuple, list, or Vector\n" + " index: Tile index (-1 for no tile)"}, {"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS, "fill(index)\n\nFill the entire layer with the specified tile index."}, {"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, @@ -1190,9 +1212,9 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb return 0; } -PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) { +PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { return NULL; } @@ -1210,8 +1232,14 @@ PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) } PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) { - int x, y, index; - if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) { + PyObject* pos_obj; + int index; + if (!PyArg_ParseTuple(args, "Oi", &pos_obj, &index)) { + return NULL; + } + + int x, y; + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { return NULL; } diff --git a/src/GridLayers.h b/src/GridLayers.h index a4656ac..02a5e43 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -200,7 +200,7 @@ class PyGridLayerAPI { public: // ColorLayer methods static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds); - static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args); + static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args); static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args); static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds); @@ -217,7 +217,7 @@ public: // TileLayer methods static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds); - static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args); + static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds); static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args); static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args); static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds); diff --git a/src/McRFPy_Automation.cpp b/src/McRFPy_Automation.cpp index a089981..7ca8fe2 100644 --- a/src/McRFPy_Automation.cpp +++ b/src/McRFPy_Automation.cpp @@ -1,6 +1,7 @@ #include "McRFPy_Automation.h" #include "McRFPy_API.h" #include "GameEngine.h" +#include "PyPositionHelper.h" #include #include #include @@ -37,20 +38,20 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) { {"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U}, {"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X}, {"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z}, - + // Numbers {"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2}, {"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5}, {"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8}, {"9", sf::Keyboard::Num9}, - + // Function keys {"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3}, {"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6}, {"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9}, {"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12}, {"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15}, - + // Special keys {"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape}, {"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter}, @@ -63,13 +64,13 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) { {"end", sf::Keyboard::End}, {"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp}, {"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown}, - + // Arrow keys {"left", sf::Keyboard::Left}, {"right", sf::Keyboard::Right}, {"up", sf::Keyboard::Up}, {"down", sf::Keyboard::Down}, - + // Modifiers {"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl}, {"ctrlright", sf::Keyboard::RControl}, @@ -79,14 +80,14 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) { {"shiftright", sf::Keyboard::RShift}, {"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem}, {"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem}, - + // Punctuation {",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period}, {"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash}, {";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote}, {"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket}, {"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal}, - + // Numpad {"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1}, {"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3}, @@ -95,14 +96,14 @@ sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) { {"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9}, {"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract}, {"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide}, - + // Other {"pause", sf::Keyboard::Pause}, {"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock {"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock {"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock }; - + auto it = keyMap.find(keyName); if (it != keyMap.end()) { return it->second; @@ -153,22 +154,22 @@ void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) { auto engine = getGameEngine(); if (!engine) return; - + sf::Event event; event.type = type; - + if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) { event.key.code = key; - event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) || + event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) || sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt); - event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) || + event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) || sf::Keyboard::isKeyPressed(sf::Keyboard::RControl); - event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || + event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::RShift); - event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) || + event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) || sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem); } - + engine->processEvent(event); } @@ -176,11 +177,11 @@ void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard:: void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) { auto engine = getGameEngine(); if (!engine) return; - + sf::Event event; event.type = sf::Event::TextEntered; event.text.unicode = unicode; - + engine->processEvent(event); } @@ -235,51 +236,77 @@ PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) { return NULL; } -// Get current mouse position +// Get current mouse position - returns Vector object PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) { - auto engine = getGameEngine(); - if (!engine || !engine->getRenderTargetPtr()) { - return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y); - } - - // In headless mode, return the simulated mouse position (#111) - if (engine->isHeadless()) { - return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y); - } - - // In windowed mode, return the actual mouse position relative to window - if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { - sf::Vector2i pos = sf::Mouse::getPosition(*window); - return Py_BuildValue("(ii)", pos.x, pos.y); - } - - // Fallback to simulated position - return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y); -} - -// Get screen size -PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) { - auto engine = getGameEngine(); - if (!engine || !engine->getRenderTargetPtr()) { - return Py_BuildValue("(ii)", 1024, 768); // Default size - } - - sf::Vector2u size = engine->getRenderTarget().getSize(); - return Py_BuildValue("(ii)", size.x, size.y); -} - -// Check if coordinates are on screen -PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + + auto engine = getGameEngine(); + if (!engine || !engine->getRenderTargetPtr()) { + x = simulated_mouse_pos.x; + y = simulated_mouse_pos.y; + } + else if (engine->isHeadless()) { + // In headless mode, return the simulated mouse position (#111) + x = simulated_mouse_pos.x; + y = simulated_mouse_pos.y; + } + else if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { + // In windowed mode, return the actual mouse position relative to window + sf::Vector2i pos = sf::Mouse::getPosition(*window); + x = pos.x; + y = pos.y; + } + else { + // Fallback to simulated position + x = simulated_mouse_pos.x; + y = simulated_mouse_pos.y; + } + + // Return a Vector object - get type from module to ensure we use the initialized type + auto vector_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!vector_type) { + PyErr_SetString(PyExc_RuntimeError, "Vector type not found in mcrfpy module"); return NULL; } - + PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", (float)x, (float)y); + Py_DECREF(vector_type); + return result; +} + +// Get screen size - returns Vector object +PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) { + // Get Vector type from module to ensure we use the initialized type + auto vector_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!vector_type) { + PyErr_SetString(PyExc_RuntimeError, "Vector type not found in mcrfpy module"); + return NULL; + } + + auto engine = getGameEngine(); + if (!engine || !engine->getRenderTargetPtr()) { + PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", 1024.0f, 768.0f); // Default size + Py_DECREF(vector_type); + return result; + } + + sf::Vector2u size = engine->getRenderTarget().getSize(); + PyObject* result = PyObject_CallFunction((PyObject*)vector_type, "ff", (float)size.x, (float)size.y); + Py_DECREF(vector_type); + return result; +} + +// Check if coordinates are on screen - accepts onScreen(x, y) or onScreen(pos) +PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args, PyObject* kwargs) { + int x, y; + if (!PyPosition_ParseInt(args, kwargs, &x, &y)) { + return NULL; + } + auto engine = getGameEngine(); if (!engine || !engine->getRenderTargetPtr()) { Py_RETURN_FALSE; } - + sf::Vector2u size = engine->getRenderTarget().getSize(); if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) { Py_RETURN_TRUE; @@ -288,84 +315,101 @@ PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) { } } -// Move mouse to position +// Move mouse to position - accepts moveTo(pos, duration) PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", "duration", NULL}; - int x, y; + static const char* kwlist[] = {"pos", "duration", NULL}; + PyObject* pos_obj; float duration = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast(kwlist), - &x, &y, &duration)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast(kwlist), + &pos_obj, &duration)) { return NULL; } - + + int x, y; + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } + // TODO: Implement smooth movement with duration injectMouseEvent(sf::Event::MouseMoved, x, y); - + if (duration > 0) { sleep_ms(static_cast(duration * 1000)); } - + Py_RETURN_NONE; } -// Move mouse relative +// Move mouse relative - accepts moveRel(offset, duration) PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL}; - int xOffset, yOffset; + static const char* kwlist[] = {"offset", "duration", NULL}; + PyObject* offset_obj; float duration = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast(kwlist), - &xOffset, &yOffset, &duration)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast(kwlist), + &offset_obj, &duration)) { return NULL; } - - // Get current position + + int xOffset, yOffset; + if (!PyPosition_FromObjectInt(offset_obj, &xOffset, &yOffset)) { + return NULL; + } + + // Get current position from Vector PyObject* pos = _position(self, NULL); if (!pos) return NULL; - + + // Extract position from Vector object int currentX, currentY; - if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) { + if (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); - + // Move to new position injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset); - + if (duration > 0) { sleep_ms(static_cast(duration * 1000)); } - + Py_RETURN_NONE; } -// Click implementation +// Click implementation - accepts click(pos, clicks, interval, button) or click() for current position PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL}; - int x = -1, y = -1; + static const char* kwlist[] = {"pos", "clicks", "interval", "button", NULL}; + PyObject* pos_obj = Py_None; int clicks = 1; float interval = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast(kwlist), - &x, &y, &clicks, &interval, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Oifs", const_cast(kwlist), + &pos_obj, &clicks, &interval, &button)) { return NULL; } - - // If no position specified, use current position - if (x == -1 || y == -1) { + + int x, y; + + // If no position specified or None, use current position + if (pos_obj == Py_None) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyArg_ParseTuple(pos, "ii", &x, &y)) { + + if (!PyPosition_FromObjectInt(pos, &x, &y)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); + } else { + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } } - + // Determine button sf::Mouse::Button sfButton = sf::Mouse::Left; if (strcmp(button, "right") == 0) { @@ -373,59 +417,61 @@ PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kw } else if (strcmp(button, "middle") == 0) { sfButton = sf::Mouse::Middle; } - + // Move to position first injectMouseEvent(sf::Event::MouseMoved, x, y); - + // Perform clicks for (int i = 0; i < clicks; i++) { if (i > 0 && interval > 0) { sleep_ms(static_cast(interval * 1000)); } - + injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton); sleep_ms(10); // Small delay between press and release injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton); } - + Py_RETURN_NONE; } -// Right click +// Right click - accepts rightClick(pos) or rightClick() for current position PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", NULL}; - int x = -1, y = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { + static const char* kwlist[] = {"pos", NULL}; + PyObject* pos_obj = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { return NULL; } - + // Build new args with button="right" PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right")); - if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); - if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); - + if (pos_obj != Py_None) { + PyDict_SetItemString(newKwargs, "pos", pos_obj); + } + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; } -// Double click +// Double click - accepts doubleClick(pos) or doubleClick() for current position PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", NULL}; - int x = -1, y = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { + static const char* kwlist[] = {"pos", NULL}; + PyObject* pos_obj = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2)); PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1)); - if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); - if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); - + if (pos_obj != Py_None) { + PyDict_SetItemString(newKwargs, "pos", pos_obj); + } + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; @@ -436,20 +482,20 @@ PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject static const char* kwlist[] = {"message", "interval", NULL}; const char* message; float interval = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast(kwlist), + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast(kwlist), &message, &interval)) { return NULL; } - + // Type each character for (size_t i = 0; message[i] != '\0'; i++) { if (i > 0 && interval > 0) { sleep_ms(static_cast(interval * 1000)); } - + char c = message[i]; - + // Handle special characters if (c == '\n') { injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter); @@ -462,7 +508,7 @@ PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject injectTextEvent(static_cast(c)); } } - + Py_RETURN_NONE; } @@ -472,13 +518,13 @@ PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, "s", &keyName)) { return NULL; } - + sf::Keyboard::Key key = stringToKey(keyName); if (key == sf::Keyboard::Unknown) { PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName); return NULL; } - + injectKeyEvent(sf::Event::KeyPressed, key); Py_RETURN_NONE; } @@ -489,13 +535,13 @@ PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, "s", &keyName)) { return NULL; } - + sf::Keyboard::Key key = stringToKey(keyName); if (key == sf::Keyboard::Unknown) { PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName); return NULL; } - + injectKeyEvent(sf::Event::KeyReleased, key); Py_RETURN_NONE; } @@ -508,7 +554,7 @@ PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) { PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key"); return NULL; } - + // Press all keys for (Py_ssize_t i = 0; i < numKeys; i++) { PyObject* keyObj = PyTuple_GetItem(args, i); @@ -516,198 +562,226 @@ PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) { if (!keyName) { return NULL; } - + sf::Keyboard::Key key = stringToKey(keyName); if (key == sf::Keyboard::Unknown) { PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName); return NULL; } - + injectKeyEvent(sf::Event::KeyPressed, key); sleep_ms(10); // Small delay between key presses } - + // Release all keys in reverse order for (Py_ssize_t i = numKeys - 1; i >= 0; i--) { PyObject* keyObj = PyTuple_GetItem(args, i); const char* keyName = PyUnicode_AsUTF8(keyObj); - + sf::Keyboard::Key key = stringToKey(keyName); injectKeyEvent(sf::Event::KeyReleased, key); sleep_ms(10); } - + Py_RETURN_NONE; } -// Scroll wheel +// Scroll wheel - accepts scroll(clicks, pos) or scroll(clicks) for current position PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"clicks", "x", "y", NULL}; + static const char* kwlist[] = {"clicks", "pos", NULL}; int clicks; - int x = -1, y = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast(kwlist), - &clicks, &x, &y)) { + PyObject* pos_obj = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|O", const_cast(kwlist), + &clicks, &pos_obj)) { return NULL; } - - // If no position specified, use current position - if (x == -1 || y == -1) { + + int x, y; + + // If no position specified or None, use current position + if (pos_obj == Py_None) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyArg_ParseTuple(pos, "ii", &x, &y)) { + + if (!PyPosition_FromObjectInt(pos, &x, &y)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); + } else { + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } } - + // Inject scroll event injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y); - + Py_RETURN_NONE; } -// Other click types using the main click function +// Other click types using the main click function - accepts middleClick(pos) or middleClick() PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", NULL}; - int x = -1, y = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { + static const char* kwlist[] = {"pos", NULL}; + PyObject* pos_obj = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle")); - if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); - if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); - + if (pos_obj != Py_None) { + PyDict_SetItemString(newKwargs, "pos", pos_obj); + } + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; } +// Triple click - accepts tripleClick(pos) or tripleClick() PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", NULL}; - int x = -1, y = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { + static const char* kwlist[] = {"pos", NULL}; + PyObject* pos_obj = Py_None; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3)); PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1)); - if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); - if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); - + if (pos_obj != Py_None) { + PyDict_SetItemString(newKwargs, "pos", pos_obj); + } + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; } -// Mouse button press/release +// Mouse button press/release - accepts mouseDown(pos, button) or mouseDown() for current position PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", "button", NULL}; - int x = -1, y = -1; + static const char* kwlist[] = {"pos", "button", NULL}; + PyObject* pos_obj = Py_None; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast(kwlist), - &x, &y, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast(kwlist), + &pos_obj, &button)) { return NULL; } - - // If no position specified, use current position - if (x == -1 || y == -1) { + + int x, y; + + // If no position specified or None, use current position + if (pos_obj == Py_None) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyArg_ParseTuple(pos, "ii", &x, &y)) { + + if (!PyPosition_FromObjectInt(pos, &x, &y)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); + } else { + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } } - + sf::Mouse::Button sfButton = sf::Mouse::Left; if (strcmp(button, "right") == 0) { sfButton = sf::Mouse::Right; } else if (strcmp(button, "middle") == 0) { sfButton = sf::Mouse::Middle; } - + injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton); Py_RETURN_NONE; } +// Mouse up - accepts mouseUp(pos, button) or mouseUp() for current position PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", "button", NULL}; - int x = -1, y = -1; + static const char* kwlist[] = {"pos", "button", NULL}; + PyObject* pos_obj = Py_None; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast(kwlist), - &x, &y, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast(kwlist), + &pos_obj, &button)) { return NULL; } - - // If no position specified, use current position - if (x == -1 || y == -1) { + + int x, y; + + // If no position specified or None, use current position + if (pos_obj == Py_None) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyArg_ParseTuple(pos, "ii", &x, &y)) { + + if (!PyPosition_FromObjectInt(pos, &x, &y)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); + } else { + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } } - + sf::Mouse::Button sfButton = sf::Mouse::Left; if (strcmp(button, "right") == 0) { sfButton = sf::Mouse::Right; } else if (strcmp(button, "middle") == 0) { sfButton = sf::Mouse::Middle; } - + injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton); Py_RETURN_NONE; } -// Drag operations +// Drag operations - accepts dragTo(pos, duration, button) PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"x", "y", "duration", "button", NULL}; - int x, y; + static const char* kwlist[] = {"pos", "duration", "button", NULL}; + PyObject* pos_obj; float duration = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast(kwlist), - &x, &y, &duration, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast(kwlist), + &pos_obj, &duration, &button)) { return NULL; } - + + int x, y; + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } + // Get current position PyObject* pos = _position(self, NULL); if (!pos) return NULL; - + int startX, startY; - if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) { + if (!PyPosition_FromObjectInt(pos, &startX, &startY)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); - - // Mouse down at current position - PyObject* downArgs = Py_BuildValue("(ii)", startX, startY); + + // Mouse down at current position - create position tuple for the call + PyObject* startPosObj = Py_BuildValue("(ii)", startX, startY); PyObject* downKwargs = PyDict_New(); + PyDict_SetItemString(downKwargs, "pos", startPosObj); PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button)); - - PyObject* downResult = _mouseDown(self, downArgs, downKwargs); - Py_DECREF(downArgs); + + PyObject* downResult = _mouseDown(self, PyTuple_New(0), downKwargs); + Py_DECREF(startPosObj); Py_DECREF(downKwargs); if (!downResult) return NULL; Py_DECREF(downResult); - + // Move to target position if (duration > 0) { // Smooth movement @@ -721,103 +795,111 @@ PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* k } else { injectMouseEvent(sf::Event::MouseMoved, x, y); } - + // Mouse up at target position - PyObject* upArgs = Py_BuildValue("(ii)", x, y); + PyObject* endPosObj = Py_BuildValue("(ii)", x, y); PyObject* upKwargs = PyDict_New(); + PyDict_SetItemString(upKwargs, "pos", endPosObj); PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button)); - - PyObject* upResult = _mouseUp(self, upArgs, upKwargs); - Py_DECREF(upArgs); + + PyObject* upResult = _mouseUp(self, PyTuple_New(0), upKwargs); + Py_DECREF(endPosObj); Py_DECREF(upKwargs); if (!upResult) return NULL; Py_DECREF(upResult); - + Py_RETURN_NONE; } +// Drag relative - accepts dragRel(offset, duration, button) PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL}; - int xOffset, yOffset; + static const char* kwlist[] = {"offset", "duration", "button", NULL}; + PyObject* offset_obj; float duration = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast(kwlist), - &xOffset, &yOffset, &duration, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast(kwlist), + &offset_obj, &duration, &button)) { return NULL; } - + + int xOffset, yOffset; + if (!PyPosition_FromObjectInt(offset_obj, &xOffset, &yOffset)) { + return NULL; + } + // Get current position PyObject* pos = _position(self, NULL); if (!pos) return NULL; - + int currentX, currentY; - if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) { + if (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); - + // Call dragTo with absolute position - PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset); + PyObject* targetPos = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset); PyObject* dragKwargs = PyDict_New(); + PyDict_SetItemString(dragKwargs, "pos", targetPos); PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration)); PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button)); - - PyObject* result = _dragTo(self, dragArgs, dragKwargs); - Py_DECREF(dragArgs); + + PyObject* result = _dragTo(self, PyTuple_New(0), dragKwargs); + Py_DECREF(targetPos); Py_DECREF(dragKwargs); - + return result; } // Method definitions for the automation module static PyMethodDef automationMethods[] = { - {"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS, + {"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS, "screenshot(filename) - Save a screenshot to the specified file"}, - - {"position", McRFPy_Automation::_position, METH_NOARGS, - "position() - Get current mouse position as (x, y) tuple"}, - {"size", McRFPy_Automation::_size, METH_NOARGS, - "size() - Get screen size as (width, height) tuple"}, - {"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS, - "onScreen(x, y) - Check if coordinates are within screen bounds"}, - - {"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS, - "moveTo(x, y, duration=0.0) - Move mouse to absolute position"}, - {"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS, - "moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"}, - {"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS, - "dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"}, - {"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS, - "dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"}, - - {"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS, - "click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"}, - {"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS, - "rightClick(x=None, y=None) - Right click at position"}, - {"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS, - "middleClick(x=None, y=None) - Middle click at position"}, - {"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS, - "doubleClick(x=None, y=None) - Double click at position"}, - {"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS, - "tripleClick(x=None, y=None) - Triple click at position"}, - {"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS, - "scroll(clicks, x=None, y=None) - Scroll wheel at position"}, - {"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS, - "mouseDown(x=None, y=None, button='left') - Press mouse button"}, - {"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS, - "mouseUp(x=None, y=None, button='left') - Release mouse button"}, - - {"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS, + + {"position", McRFPy_Automation::_position, METH_NOARGS, + "position() - Get current mouse position as Vector"}, + {"size", McRFPy_Automation::_size, METH_NOARGS, + "size() - Get screen size as Vector"}, + {"onScreen", (PyCFunction)McRFPy_Automation::_onScreen, METH_VARARGS | METH_KEYWORDS, + "onScreen(pos) - Check if position is within screen bounds. Accepts (x,y) tuple, [x,y] list, or Vector."}, + + {"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS, + "moveTo(pos, duration=0.0) - Move mouse to position. Accepts (x,y) tuple, [x,y] list, or Vector."}, + {"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS, + "moveRel(offset, duration=0.0) - Move mouse relative to current position. Accepts (x,y) tuple, [x,y] list, or Vector."}, + {"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS, + "dragTo(pos, duration=0.0, button='left') - Drag mouse to position. Accepts (x,y) tuple, [x,y] list, or Vector."}, + {"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS, + "dragRel(offset, duration=0.0, button='left') - Drag mouse relative to current position. Accepts (x,y) tuple, [x,y] list, or Vector."}, + + {"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS, + "click(pos=None, clicks=1, interval=0.0, button='left') - Click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS, + "rightClick(pos=None) - Right click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS, + "middleClick(pos=None) - Middle click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS, + "doubleClick(pos=None) - Double click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS, + "tripleClick(pos=None) - Triple click at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS, + "scroll(clicks, pos=None) - Scroll wheel at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS, + "mouseDown(pos=None, button='left') - Press mouse button at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + {"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS, + "mouseUp(pos=None, button='left') - Release mouse button at position. Accepts (x,y) tuple, [x,y] list, Vector, or None for current position."}, + + {"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS, "typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"}, - {"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS, + {"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS, "hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"}, - {"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS, + {"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS, "keyDown(key) - Press and hold a key"}, - {"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS, + {"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS, "keyUp(key) - Release a key"}, - + {NULL, NULL, 0, NULL} }; @@ -836,6 +918,6 @@ PyObject* McRFPy_Automation::init_automation_module() { if (module == NULL) { return NULL; } - + return module; -} \ No newline at end of file +} diff --git a/src/McRFPy_Automation.h b/src/McRFPy_Automation.h index 02a6799..a090fc3 100644 --- a/src/McRFPy_Automation.h +++ b/src/McRFPy_Automation.h @@ -20,7 +20,7 @@ public: // Mouse position and screen info static PyObject* _position(PyObject* self, PyObject* args); static PyObject* _size(PyObject* self, PyObject* args); - static PyObject* _onScreen(PyObject* self, PyObject* args); + static PyObject* _onScreen(PyObject* self, PyObject* args, PyObject* kwargs); // Mouse movement static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs); diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index 6fed830..1c6b1ae 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -1,6 +1,7 @@ #include "PyCallable.h" #include "McRFPy_API.h" #include "GameEngine.h" +#include "PyVector.h" PyCallable::PyCallable(PyObject* _target) { @@ -49,7 +50,24 @@ PyClickCallable::PyClickCallable() void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action) { - PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), action.c_str()); + // Create a Vector object for the position - must fetch the finalized type from the module + PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (!vector_type) { + std::cerr << "Failed to get Vector type for click callback" << std::endl; + PyErr_Print(); + PyErr_Clear(); + return; + } + PyObject* pos = PyObject_CallFunction(vector_type, "ff", mousepos.x, mousepos.y); + Py_DECREF(vector_type); + if (!pos) { + std::cerr << "Failed to create Vector object for click callback" << std::endl; + PyErr_Print(); + PyErr_Clear(); + return; + } + PyObject* args = Py_BuildValue("(Oss)", pos, button.c_str(), action.c_str()); + Py_DECREF(pos); // Py_BuildValue increments the refcount PyObject* retval = PyCallable::call(args, NULL); if (!retval) { diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 1931626..9ccadd6 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -1,13 +1,14 @@ #include "PyDrawable.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" +#include "PyPositionHelper.h" // Click property getter static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) { - if (!self->data->click_callable) + if (!self->data->click_callable) Py_RETURN_NONE; - + PyObject* ptr = self->data->click_callable->borrow(); if (ptr && ptr != Py_None) return ptr; @@ -35,20 +36,20 @@ static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure) return PyLong_FromLong(self->data->z_index); } -// Z-index property setter +// Z-index property setter static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure) { if (!PyLong_Check(value)) { PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); return -1; } - + int val = PyLong_AsLong(value); self->data->z_index = val; - + // Mark scene as needing resort self->data->notifyZIndexChanged(); - + return 0; } @@ -65,7 +66,7 @@ static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); return -1; } - + self->data->visible = (value == Py_True); return 0; } @@ -88,11 +89,11 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* PyErr_SetString(PyExc_TypeError, "opacity must be a number"); return -1; } - + // Clamp to valid range if (val < 0.0f) val = 0.0f; if (val > 1.0f) val = 1.0f; - + self->data->opacity = val; return 0; } @@ -102,7 +103,7 @@ static PyGetSetDef PyDrawable_getsetters[] = { {"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, MCRF_PROPERTY(on_click, "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." + "Function receives (pos: Vector, button: str, action: str)." ), NULL}, {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, MCRF_PROPERTY(z_index, @@ -130,25 +131,25 @@ static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUS } // move method implementation (#98) -static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args) +static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args, PyObject* kwds) { float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { + if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) { return NULL; } - + self->data->move(dx, dy); Py_RETURN_NONE; } // resize method implementation (#98) -static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) +static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args, PyObject* kwds) { float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { + if (!PyPosition_ParseFloat(args, kwds, &w, &h)) { return NULL; } - + self->data->resize(w, h); Py_RETURN_NONE; } @@ -162,23 +163,27 @@ static PyMethodDef PyDrawable_methods[] = { MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") )}, - {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, + {"move", (PyCFunction)PyDrawable_move, METH_VARARGS | METH_KEYWORDS, MCRF_METHOD(Drawable, move, - MCRF_SIG("(dx: float, dy: float)", "None"), + MCRF_SIG("(dx, dy) or (delta)", "None"), MCRF_DESC("Move the element by a relative offset."), MCRF_ARGS_START - MCRF_ARG("dx", "Horizontal offset in pixels") - MCRF_ARG("dy", "Vertical offset in pixels") - MCRF_NOTE("This modifies the x and y position properties by the given amounts.") + MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)") + MCRF_ARG("dy", "Vertical offset in pixels (or use delta)") + MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)") + MCRF_NOTE("This modifies the x and y position properties by the given amounts. " + "Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).") )}, - {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, + {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS | METH_KEYWORDS, MCRF_METHOD(Drawable, resize, - MCRF_SIG("(width: float, height: float)", "None"), + MCRF_SIG("(width, height) or (size)", "None"), MCRF_DESC("Resize the element to new dimensions."), MCRF_ARGS_START - MCRF_ARG("width", "New width in pixels") - MCRF_ARG("height", "New height in pixels") - MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") + MCRF_ARG("width", "New width in pixels (or use size)") + MCRF_ARG("height", "New height in pixels (or use size)") + MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)") + MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content. " + "Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).") )}, {NULL} // Sentinel }; @@ -208,4 +213,4 @@ namespace mcrfpydef { .tp_init = (initproc)PyDrawable_init, .tp_new = PyType_GenericNew, }; -} \ No newline at end of file +} diff --git a/src/PyPositionHelper.h b/src/PyPositionHelper.h index 1f46820..92bc9ee 100644 --- a/src/PyPositionHelper.h +++ b/src/PyPositionHelper.h @@ -3,7 +3,32 @@ #include "PyVector.h" #include "McRFPy_API.h" -// Helper class for standardized position argument parsing across UI classes +// ============================================================================ +// PyPositionHelper - Reusable position argument parsing for McRogueFace API +// ============================================================================ +// +// This helper provides standardized parsing for position arguments that can be +// specified in multiple formats: +// - Two separate args: func(x, y) +// - A tuple: func((x, y)) +// - A Vector object: func(Vector(x, y)) +// - Any iterable with len() == 2: func([x, y]) +// - Keyword args: func(x=x, y=y) or func(pos=(x,y)) +// +// Usage patterns: +// // For methods with only position args (like Grid.at()): +// int x, y; +// if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL; +// +// // For extracting position from a single PyObject: +// float x, y; +// if (!PyPosition_FromObject(obj, &x, &y)) return NULL; +// +// // For more complex parsing with additional args: +// auto result = PyPositionHelper::parse_position(args, kwds); +// if (!result.has_position) { ... } +// ============================================================================ + class PyPositionHelper { public: // Template structure for parsing results @@ -12,33 +37,315 @@ public: float y = 0.0f; bool has_position = false; }; - + struct ParseResultInt { int x = 0; int y = 0; bool has_position = false; }; - + +private: + // Internal helper: extract two numeric values from a 2-element iterable + // Returns true on success, false on failure (does NOT set Python error) + static bool extract_from_iterable(PyObject* obj, float* out_x, float* out_y) { + // First check if it's a Vector (most specific) + PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + if (PyObject_IsInstance(obj, (PyObject*)vector_type)) { + PyVectorObject* vec = (PyVectorObject*)obj; + *out_x = vec->data.x; + *out_y = vec->data.y; + Py_DECREF(vector_type); + return true; + } + Py_DECREF(vector_type); + } else { + PyErr_Clear(); // Clear any error from GetAttrString + } + + // Check for tuple (common case, optimized) + if (PyTuple_Check(obj)) { + if (PyTuple_Size(obj) != 2) return false; + PyObject* x_obj = PyTuple_GetItem(obj, 0); + PyObject* y_obj = PyTuple_GetItem(obj, 1); + if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) { + return false; + } + return true; + } + + // Check for list (also common) + if (PyList_Check(obj)) { + if (PyList_Size(obj) != 2) return false; + PyObject* x_obj = PyList_GetItem(obj, 0); + PyObject* y_obj = PyList_GetItem(obj, 1); + if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) { + return false; + } + return true; + } + + // Generic iterable fallback: check __len__ and index + // This handles any object that implements sequence protocol + if (PySequence_Check(obj)) { + Py_ssize_t len = PySequence_Size(obj); + if (len != 2) { + PyErr_Clear(); // Clear size error + return false; + } + PyObject* x_obj = PySequence_GetItem(obj, 0); + if (!x_obj) { PyErr_Clear(); return false; } + PyObject* y_obj = PySequence_GetItem(obj, 1); + if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; } + + bool success = extract_number(x_obj, out_x) && extract_number(y_obj, out_y); + Py_DECREF(x_obj); + Py_DECREF(y_obj); + return success; + } + + return false; + } + + // Internal helper: extract integer values from a 2-element iterable + static bool extract_from_iterable_int(PyObject* obj, int* out_x, int* out_y) { + // First check if it's a Vector + PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString( + McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + if (PyObject_IsInstance(obj, (PyObject*)vector_type)) { + PyVectorObject* vec = (PyVectorObject*)obj; + *out_x = static_cast(vec->data.x); + *out_y = static_cast(vec->data.y); + Py_DECREF(vector_type); + return true; + } + Py_DECREF(vector_type); + } else { + PyErr_Clear(); + } + + // Check for tuple + if (PyTuple_Check(obj)) { + if (PyTuple_Size(obj) != 2) return false; + PyObject* x_obj = PyTuple_GetItem(obj, 0); + PyObject* y_obj = PyTuple_GetItem(obj, 1); + if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) { + return false; + } + return true; + } + + // Check for list + if (PyList_Check(obj)) { + if (PyList_Size(obj) != 2) return false; + PyObject* x_obj = PyList_GetItem(obj, 0); + PyObject* y_obj = PyList_GetItem(obj, 1); + if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) { + return false; + } + return true; + } + + // Generic sequence fallback + if (PySequence_Check(obj)) { + Py_ssize_t len = PySequence_Size(obj); + if (len != 2) { + PyErr_Clear(); + return false; + } + PyObject* x_obj = PySequence_GetItem(obj, 0); + if (!x_obj) { PyErr_Clear(); return false; } + PyObject* y_obj = PySequence_GetItem(obj, 1); + if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; } + + bool success = extract_int(x_obj, out_x) && extract_int(y_obj, out_y); + Py_DECREF(x_obj); + Py_DECREF(y_obj); + return success; + } + + return false; + } + + // Extract a float from a numeric Python object + static bool extract_number(PyObject* obj, float* out) { + if (PyFloat_Check(obj)) { + *out = static_cast(PyFloat_AsDouble(obj)); + return true; + } + if (PyLong_Check(obj)) { + *out = static_cast(PyLong_AsLong(obj)); + return true; + } + return false; + } + + // Extract an int from a numeric Python object (integers only) + static bool extract_int(PyObject* obj, int* out) { + if (PyLong_Check(obj)) { + *out = static_cast(PyLong_AsLong(obj)); + return true; + } + // Also accept float but only if it's a whole number + if (PyFloat_Check(obj)) { + double val = PyFloat_AsDouble(obj); + if (val == static_cast(static_cast(val))) { + *out = static_cast(val); + return true; + } + } + return false; + } + +public: + // ======================================================================== + // Simple API: Parse position from a single PyObject + // ======================================================================== + + // Extract float position from any supported format + // Sets Python error and returns false on failure + static bool FromObject(PyObject* obj, float* out_x, float* out_y) { + if (extract_from_iterable(obj, out_x, out_y)) { + return true; + } + PyErr_SetString(PyExc_TypeError, + "Expected a position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence"); + return false; + } + + // Extract integer position from any supported format + // Sets Python error and returns false on failure + static bool FromObjectInt(PyObject* obj, int* out_x, int* out_y) { + if (extract_from_iterable_int(obj, out_x, out_y)) { + return true; + } + PyErr_SetString(PyExc_TypeError, + "Expected integer position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence"); + return false; + } + + // ======================================================================== + // Method argument API: Parse position from args tuple + // ======================================================================== + + // Parse float position from method arguments + // Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable) + // Sets Python error and returns false on failure + static bool ParseFloat(PyObject* args, PyObject* kwds, float* out_x, float* out_y) { + // First try keyword arguments + if (kwds) { + PyObject* x_obj = PyDict_GetItemString(kwds, "x"); + PyObject* y_obj = PyDict_GetItemString(kwds, "y"); + PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); + + if (x_obj && y_obj) { + if (extract_number(x_obj, out_x) && extract_number(y_obj, out_y)) { + return true; + } + } + + if (pos_obj) { + if (extract_from_iterable(pos_obj, out_x, out_y)) { + return true; + } + } + } + + Py_ssize_t nargs = PyTuple_Size(args); + + // Try two separate numeric arguments: func(x, y) + if (nargs >= 2) { + PyObject* first = PyTuple_GetItem(args, 0); + PyObject* second = PyTuple_GetItem(args, 1); + + if (extract_number(first, out_x) && extract_number(second, out_y)) { + return true; + } + } + + // Try single iterable argument: func((x, y)) or func(Vector) or func([x, y]) + if (nargs == 1) { + PyObject* first = PyTuple_GetItem(args, 0); + if (extract_from_iterable(first, out_x, out_y)) { + return true; + } + } + + PyErr_SetString(PyExc_TypeError, + "Position can be specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence"); + return false; + } + + // Parse integer position from method arguments + // Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable) + // Sets Python error and returns false on failure + static bool ParseInt(PyObject* args, PyObject* kwds, int* out_x, int* out_y) { + // First try keyword arguments + if (kwds) { + PyObject* x_obj = PyDict_GetItemString(kwds, "x"); + PyObject* y_obj = PyDict_GetItemString(kwds, "y"); + PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); + + if (x_obj && y_obj) { + if (extract_int(x_obj, out_x) && extract_int(y_obj, out_y)) { + return true; + } + } + + if (pos_obj) { + if (extract_from_iterable_int(pos_obj, out_x, out_y)) { + return true; + } + } + } + + Py_ssize_t nargs = PyTuple_Size(args); + + // Try two separate integer arguments: func(x, y) + if (nargs >= 2) { + PyObject* first = PyTuple_GetItem(args, 0); + PyObject* second = PyTuple_GetItem(args, 1); + + if (extract_int(first, out_x) && extract_int(second, out_y)) { + return true; + } + } + + // Try single iterable argument: func((x, y)) or func(Vector) or func([x, y]) + if (nargs == 1) { + PyObject* first = PyTuple_GetItem(args, 0); + if (extract_from_iterable_int(first, out_x, out_y)) { + return true; + } + } + + PyErr_SetString(PyExc_TypeError, + "Position must be integers specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence"); + return false; + } + + // ======================================================================== + // Legacy struct-based API (for compatibility with existing code) + // ======================================================================== + // Parse position from multiple formats for UI class constructors // Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector - static ParseResult parse_position(PyObject* args, PyObject* kwds, - int* arg_index = nullptr) + static ParseResult parse_position(PyObject* args, PyObject* kwds, + int* arg_index = nullptr) { ParseResult result; float x = 0.0f, y = 0.0f; - PyObject* pos_obj = nullptr; int start_index = arg_index ? *arg_index : 0; - + // Check for positional tuple (x, y) first - if (!kwds && PyTuple_Size(args) > start_index + 1) { + if (PyTuple_Size(args) > start_index + 1) { PyObject* first = PyTuple_GetItem(args, start_index); PyObject* second = PyTuple_GetItem(args, start_index + 1); - + // Check if both are numbers - if ((PyFloat_Check(first) || PyLong_Check(first)) && - (PyFloat_Check(second) || PyLong_Check(second))) { - x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first); - y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second); + if (extract_number(first, &x) && extract_number(second, &y)) { result.x = x; result.y = y; result.has_position = true; @@ -46,119 +353,100 @@ public: return result; } } - - // Check for single positional argument that might be tuple or Vector - if (!kwds && PyTuple_Size(args) > start_index) { + + // Check for single positional argument that might be tuple, list, or Vector + if (PyTuple_Size(args) > start_index) { PyObject* first = PyTuple_GetItem(args, start_index); - PyVectorObject* vec = PyVector::from_arg(first); - if (vec) { - result.x = vec->data.x; - result.y = vec->data.y; + if (extract_from_iterable(first, &x, &y)) { + result.x = x; + result.y = y; result.has_position = true; if (arg_index) *arg_index += 1; return result; } } - + // Try keyword arguments if (kwds) { PyObject* x_obj = PyDict_GetItemString(kwds, "x"); PyObject* y_obj = PyDict_GetItemString(kwds, "y"); PyObject* pos_kw = PyDict_GetItemString(kwds, "pos"); - + if (x_obj && y_obj) { - if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && - (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { - result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); - result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); + if (extract_number(x_obj, &x) && extract_number(y_obj, &y)) { + result.x = x; + result.y = y; result.has_position = true; return result; } } - + if (pos_kw) { - PyVectorObject* vec = PyVector::from_arg(pos_kw); - if (vec) { - result.x = vec->data.x; - result.y = vec->data.y; + if (extract_from_iterable(pos_kw, &x, &y)) { + result.x = x; + result.y = y; result.has_position = true; return result; } } } - + return result; } - + // Parse integer position for Grid.at() and similar - static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds) + static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds) { ParseResultInt result; - - // Check for positional tuple (x, y) first - if (!kwds && PyTuple_Size(args) >= 2) { - PyObject* first = PyTuple_GetItem(args, 0); - PyObject* second = PyTuple_GetItem(args, 1); - - if (PyLong_Check(first) && PyLong_Check(second)) { - result.x = PyLong_AsLong(first); - result.y = PyLong_AsLong(second); - result.has_position = true; - return result; - } + int x = 0, y = 0; + + // Try the new simplified parser first + if (ParseInt(args, kwds, &x, &y)) { + result.x = x; + result.y = y; + result.has_position = true; + PyErr_Clear(); // Clear any error set by ParseInt } - - // Check for single tuple argument - if (!kwds && PyTuple_Size(args) == 1) { - PyObject* first = PyTuple_GetItem(args, 0); - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - PyObject* x_obj = PyTuple_GetItem(first, 0); - PyObject* y_obj = PyTuple_GetItem(first, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - result.x = PyLong_AsLong(x_obj); - result.y = PyLong_AsLong(y_obj); - result.has_position = true; - return result; - } - } - } - - // Try keyword arguments - if (kwds) { - PyObject* x_obj = PyDict_GetItemString(kwds, "x"); - PyObject* y_obj = PyDict_GetItemString(kwds, "y"); - PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); - - if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - result.x = PyLong_AsLong(x_obj); - result.y = PyLong_AsLong(y_obj); - result.has_position = true; - return result; - } - - if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if (PyLong_Check(x_val) && PyLong_Check(y_val)) { - result.x = PyLong_AsLong(x_val); - result.y = PyLong_AsLong(y_val); - result.has_position = true; - return result; - } - } - } - + return result; } - + // Error message helper static void set_position_error() { PyErr_SetString(PyExc_TypeError, - "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector"); + "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence"); } - + static void set_position_int_error() { PyErr_SetString(PyExc_TypeError, - "Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values"); + "Position must be integers specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence"); } -}; \ No newline at end of file +}; + +// ============================================================================ +// Convenience macros/functions for common use patterns +// ============================================================================ + +// Parse integer position from method args - simplest API +// Usage: if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL; +inline bool PyPosition_ParseInt(PyObject* args, PyObject* kwds, int* x, int* y) { + return PyPositionHelper::ParseInt(args, kwds, x, y); +} + +// Parse float position from method args +// Usage: if (!PyPosition_ParseFloat(args, kwds, &x, &y)) return NULL; +inline bool PyPosition_ParseFloat(PyObject* args, PyObject* kwds, float* x, float* y) { + return PyPositionHelper::ParseFloat(args, kwds, x, y); +} + +// Extract integer position from a single Python object +// Usage: if (!PyPosition_FromObjectInt(obj, &x, &y)) return NULL; +inline bool PyPosition_FromObjectInt(PyObject* obj, int* x, int* y) { + return PyPositionHelper::FromObjectInt(obj, x, y); +} + +// Extract float position from a single Python object +// Usage: if (!PyPosition_FromObject(obj, &x, &y)) return NULL; +inline bool PyPosition_FromObject(PyObject* obj, float* x, float* y) { + return PyPositionHelper::FromObject(obj, x, y); +} \ No newline at end of file diff --git a/src/UIArc.cpp b/src/UIArc.cpp index e2e93ae..a65054d 100644 --- a/src/UIArc.cpp +++ b/src/UIArc.cpp @@ -430,7 +430,7 @@ PyGetSetDef UIArc::getsetters[] = { {"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness, "Line thickness", NULL}, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - "Callable executed when arc is clicked.", (void*)PyObjectsEnum::UIARC}, + "Callable executed when arc is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UIARC}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, diff --git a/src/UIBase.h b/src/UIBase.h index 570571b..36f1ab7 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,6 +1,7 @@ #pragma once #include "Python.h" #include "McRFPy_Doc.h" +#include "PyPositionHelper.h" #include class UIEntity; @@ -52,23 +53,23 @@ static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args)) // move method implementation (#98) template -static PyObject* UIDrawable_move(T* self, PyObject* args) +static PyObject* UIDrawable_move(T* self, PyObject* args, PyObject* kwds) { float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { + if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) { return NULL; } - + self->data->move(dx, dy); Py_RETURN_NONE; } // resize method implementation (#98) template -static PyObject* UIDrawable_resize(T* self, PyObject* args) +static PyObject* UIDrawable_resize(T* self, PyObject* args, PyObject* kwds) { float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { + if (!PyPosition_ParseFloat(args, kwds, &w, &h)) { return NULL; } @@ -97,23 +98,25 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds) MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \ MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \ )}, \ - {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ + {"move", (PyCFunction)UIDrawable_move, METH_VARARGS | METH_KEYWORDS, \ MCRF_METHOD(Drawable, move, \ - MCRF_SIG("(dx: float, dy: float)", "None"), \ + MCRF_SIG("(dx, dy) or (delta)", "None"), \ MCRF_DESC("Move the element by a relative offset."), \ MCRF_ARGS_START \ - MCRF_ARG("dx", "Horizontal offset in pixels") \ - MCRF_ARG("dy", "Vertical offset in pixels") \ - MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \ + MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)") \ + MCRF_ARG("dy", "Vertical offset in pixels (or use delta)") \ + MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)") \ + MCRF_NOTE("Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).") \ )}, \ - {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ + {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS | METH_KEYWORDS, \ MCRF_METHOD(Drawable, resize, \ - MCRF_SIG("(width: float, height: float)", "None"), \ + MCRF_SIG("(width, height) or (size)", "None"), \ MCRF_DESC("Resize the element to new dimensions."), \ MCRF_ARGS_START \ - MCRF_ARG("width", "New width in pixels") \ - MCRF_ARG("height", "New height in pixels") \ - MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \ + MCRF_ARG("width", "New width in pixels (or use size)") \ + MCRF_ARG("height", "New height in pixels (or use size)") \ + MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)") \ + MCRF_NOTE("Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).") \ )} // Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives) @@ -222,12 +225,12 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) {"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \ MCRF_PROPERTY(on_enter, \ "Callback for mouse enter events. " \ - "Called with (x, y, button, action) when mouse enters this element's bounds." \ + "Called with (pos: Vector, button: str, action: str) when mouse enters this element's bounds." \ ), (void*)type_enum}, \ {"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \ MCRF_PROPERTY(on_exit, \ "Callback for mouse exit events. " \ - "Called with (x, y, button, action) when mouse leaves this element's bounds." \ + "Called with (pos: Vector, button: str, action: str) when mouse leaves this element's bounds." \ ), (void*)type_enum}, \ {"hovered", (getter)UIDrawable::get_hovered, NULL, \ MCRF_PROPERTY(hovered, \ @@ -237,7 +240,7 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) {"on_move", (getter)UIDrawable::get_on_move, (setter)UIDrawable::set_on_move, \ MCRF_PROPERTY(on_move, \ "Callback for mouse movement within bounds. " \ - "Called with (x, y, button, action) for each mouse movement while inside. " \ + "Called with (pos: Vector, button: str, action: str) for each mouse movement while inside. " \ "Performance note: Called frequently during movement - keep handlers fast." \ ), (void*)type_enum} diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 155342a..d5fb758 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -280,7 +280,7 @@ PyGetSetDef UICaption::getsetters[] = { {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, MCRF_PROPERTY(on_click, "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." + "Function receives (pos: Vector, button: str, action: str)." ), (void*)PyObjectsEnum::UICAPTION}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, MCRF_PROPERTY(z_index, diff --git a/src/UICircle.cpp b/src/UICircle.cpp index 7cfa5da..04322c0 100644 --- a/src/UICircle.cpp +++ b/src/UICircle.cpp @@ -385,7 +385,7 @@ PyGetSetDef UICircle::getsetters[] = { {"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline, "Outline thickness (0 for no outline)", NULL}, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - "Callable executed when circle is clicked.", (void*)PyObjectsEnum::UICIRCLE}, + "Callable executed when circle is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UICIRCLE}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE}, {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index da5b8cb..fe13678 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -10,6 +10,7 @@ #include "Animation.h" #include "PyAnimation.h" #include "PyEasing.h" +#include "PyPositionHelper.h" // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -94,18 +95,17 @@ void UIEntity::updateVisibility() } } -PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { +PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(o, "ii", &x, &y)) { - PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)"); - return NULL; + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + return NULL; // Error already set by PyPosition_ParseInt } - + if (self->data->grid == NULL) { PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid"); return NULL; } - + // Lazy initialize gridstate if needed if (self->data->gridstate.size() == 0) { self->data->gridstate.resize(self->data->grid->grid_x * self->data->grid->grid_y); @@ -115,13 +115,13 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { state.discovered = false; } } - + // Bounds check if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) { PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y); return NULL; } - + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState"); auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0); Py_DECREF(type); @@ -590,21 +590,14 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr}; - int target_x = -1, target_y = -1; - - // Parse arguments - support both target_x/target_y and x/y parameter names - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast(keywords), - &target_x, &target_y)) { - PyErr_Clear(); - // Try alternative parameter names - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast(keywords), - &target_x, &target_y, &target_x, &target_y)) { - PyErr_SetString(PyExc_TypeError, "path_to() requires target_x and target_y integer arguments"); - return NULL; - } + int target_x, target_y; + + // Parse position using flexible position helper + // Supports: path_to(x, y), path_to((x, y)), path_to(pos=(x, y)), path_to(Vector(x, y)) + if (!PyPosition_ParseInt(args, kwds, &target_x, &target_y)) { + return NULL; // Error already set by PyPosition_ParseInt } - + // Check if entity has a grid if (!self->data || !self->data->grid) { PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths"); @@ -743,19 +736,32 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO } PyMethodDef UIEntity::methods[] = { - {"at", (PyCFunction)UIEntity::at, METH_O}, + {"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS, + "at(x, y) or at(pos) -> GridPointState\n\n" + "Get the grid point state at the specified position.\n\n" + "Args:\n" + " x, y: Grid coordinates as two integers, OR\n" + " pos: Grid coordinates as tuple, list, or Vector\n\n" + "Returns:\n" + " GridPointState for the entity's view of that grid cell.\n\n" + "Example:\n" + " state = entity.at(5, 3)\n" + " state = entity.at((5, 3))\n" + " state = entity.at(pos=(5, 3))"}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, - "path_to(x: int, y: int) -> bool\n\n" - "Find and follow path to target position using A* pathfinding.\n\n" + "path_to(x, y) or path_to(target) -> list\n\n" + "Find a path to the target position using Dijkstra pathfinding.\n\n" "Args:\n" - " x: Target X coordinate\n" - " y: Target Y coordinate\n\n" + " x, y: Target coordinates as two integers, OR\n" + " target: Target coordinates as tuple, list, or Vector\n\n" "Returns:\n" - " True if a path was found and the entity started moving, False otherwise\n\n" - "The entity will automatically move along the path over multiple frames.\n" - "Call this again to change the target or repath."}, + " List of (x, y) tuples representing the path.\n\n" + "Example:\n" + " path = entity.path_to(10, 5)\n" + " path = entity.path_to((10, 5))\n" + " path = entity.path_to(pos=(10, 5))"}, {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "update_visibility() -> None\n\n" "Update entity's visibility state based on current FOV.\n\n" @@ -799,19 +805,32 @@ PyMethodDef UIEntity_all_methods[] = { MCRF_RAISES("ValueError", "If property name is not valid for Entity (x, y, sprite_scale, sprite_index)") MCRF_NOTE("Entity animations use grid coordinates for x/y, not pixel coordinates.") )}, - {"at", (PyCFunction)UIEntity::at, METH_O}, + {"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS, + "at(x, y) or at(pos) -> GridPointState\n\n" + "Get the grid point state at the specified position.\n\n" + "Args:\n" + " x, y: Grid coordinates as two integers, OR\n" + " pos: Grid coordinates as tuple, list, or Vector\n\n" + "Returns:\n" + " GridPointState for the entity's view of that grid cell.\n\n" + "Example:\n" + " state = entity.at(5, 3)\n" + " state = entity.at((5, 3))\n" + " state = entity.at(pos=(5, 3))"}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS, - "path_to(x: int, y: int) -> bool\n\n" - "Find and follow path to target position using A* pathfinding.\n\n" + "path_to(x, y) or path_to(target) -> list\n\n" + "Find a path to the target position using Dijkstra pathfinding.\n\n" "Args:\n" - " x: Target X coordinate\n" - " y: Target Y coordinate\n\n" + " x, y: Target coordinates as two integers, OR\n" + " target: Target coordinates as tuple, list, or Vector\n\n" "Returns:\n" - " True if a path was found and the entity started moving, False otherwise\n\n" - "The entity will automatically move along the path over multiple frames.\n" - "Call this again to change the target or repath."}, + " List of (x, y) tuples representing the path.\n\n" + "Example:\n" + " path = entity.path_to(10, 5)\n" + " path = entity.path_to((10, 5))\n" + " path = entity.path_to(pos=(10, 5))"}, {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "update_visibility() -> None\n\n" "Update entity's visibility state based on current FOV.\n\n" diff --git a/src/UIEntity.h b/src/UIEntity.h index f3313d9..757659c 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -89,7 +89,7 @@ public: void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; } void resize(float w, float h) { /* Entities don't support direct resizing */ } - static PyObject* at(PyUIEntityObject* self, PyObject* o); + static PyObject* at(PyUIEntityObject* self, PyObject* args, PyObject* kwds); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 7103212..16dfada 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -444,7 +444,7 @@ PyGetSetDef UIFrame::getsetters[] = { {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, MCRF_PROPERTY(on_click, "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." + "Function receives (pos: Vector, button: str, action: str)." ), (void*)PyObjectsEnum::UIFRAME}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, MCRF_PROPERTY(z_index, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index dfdf809..02c4b2a 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -5,6 +5,7 @@ #include "UIEntity.h" #include "Profiler.h" #include "PyFOV.h" +#include "PyPositionHelper.h" // For standardized position argument parsing #include #include // #142 - for std::floor, std::isnan #include // #150 - for strcmp @@ -685,15 +686,24 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) // Only fire if within valid grid bounds if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) { - PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y); - PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); - Py_DECREF(args); - if (!result) { - std::cerr << "Cell click callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else { - Py_DECREF(result); + // Create Vector object for cell position - must fetch finalized type from module + PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y); + Py_DECREF(vector_type); + if (cell_pos) { + PyObject* args = Py_BuildValue("(O)", cell_pos); + Py_DECREF(cell_pos); + PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); + Py_DECREF(args); + if (!result) { + std::cerr << "Cell click callback raised an exception:" << std::endl; + PyErr_Print(); + PyErr_Clear(); + } else { + Py_DECREF(result); + } + } } } } @@ -709,15 +719,24 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) // Only fire if within valid grid bounds if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) { - PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y); - PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); - Py_DECREF(args); - if (!result) { - std::cerr << "Cell click callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else { - Py_DECREF(result); + // Create Vector object for cell position - must fetch finalized type from module + PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)cell_x, (float)cell_y); + Py_DECREF(vector_type); + if (cell_pos) { + PyObject* args = Py_BuildValue("(O)", cell_pos); + Py_DECREF(cell_pos); + PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args); + Py_DECREF(args); + if (!result) { + std::cerr << "Cell click callback raised an exception:" << std::endl; + PyErr_Print(); + PyErr_Clear(); + } else { + Py_DECREF(result); + } + } } // Don't return this - no click_callable to call } @@ -1141,36 +1160,14 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) { PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"x", "y", nullptr}; - int x = 0, y = 0; - - // First try to parse as two integers - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast(keywords), &x, &y)) { - PyErr_Clear(); - - // Try to parse as a single tuple argument - PyObject* pos_tuple = nullptr; - if (PyArg_ParseTuple(args, "O", &pos_tuple)) { - if (PyTuple_Check(pos_tuple) && PyTuple_Size(pos_tuple) == 2) { - PyObject* x_obj = PyTuple_GetItem(pos_tuple, 0); - PyObject* y_obj = PyTuple_GetItem(pos_tuple, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - x = PyLong_AsLong(x_obj); - y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "Grid indices must be integers"); - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers"); - return NULL; - } - } else { - PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers"); - return NULL; - } + int x, y; + + // Use the flexible position parsing helper - accepts: + // at(x, y), at((x, y)), at([x, y]), at(Vector(x, y)), at(pos=(x, y)), etc. + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + return NULL; // Error already set by PyPosition_ParseInt } - + // Range validation if (x < 0 || x >= self->data->grid_x) { PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_x); @@ -1349,16 +1346,22 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure) // Python API implementations for TCOD functionality PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL}; - int x, y, radius = 0; + static const char* kwlist[] = {"pos", "radius", "light_walls", "algorithm", NULL}; + PyObject* pos_obj = NULL; + int radius = 0; int light_walls = 1; int algorithm = FOV_BASIC; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast(kwlist), - &x, &y, &radius, &light_walls, &algorithm)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast(kwlist), + &pos_obj, &radius, &light_walls, &algorithm)) { return NULL; } - + + int x, y; + if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + return NULL; + } + // Compute FOV self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm); @@ -1367,33 +1370,42 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* Py_RETURN_NONE; } -PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args) +PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { return NULL; } - + bool in_fov = self->data->isInFOV(x, y); return PyBool_FromLong(in_fov); } PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; - int x1, y1, x2, y2; + static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL}; + PyObject* start_obj = NULL; + PyObject* end_obj = NULL; float diagonal_cost = 1.41f; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast(kwlist), - &x1, &y1, &x2, &y2, &diagonal_cost)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast(kwlist), + &start_obj, &end_obj, &diagonal_cost)) { return NULL; } - + + int x1, y1, x2, y2; + if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) { + return NULL; + } + if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) { + return NULL; + } + std::vector> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost); - + PyObject* path_list = PyList_New(path.size()); if (!path_list) return NULL; - + for (size_t i = 0; i < path.size(); i++) { PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second); if (!coord) { @@ -1402,80 +1414,93 @@ PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* k } PyList_SET_ITEM(path_list, i, coord); } - + return path_list; } PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL}; - int root_x, root_y; + static const char* kwlist[] = {"root", "diagonal_cost", NULL}; + PyObject* root_obj = NULL; float diagonal_cost = 1.41f; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast(kwlist), - &root_x, &root_y, &diagonal_cost)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast(kwlist), + &root_obj, &diagonal_cost)) { return NULL; } - + + int root_x, root_y; + if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) { + return NULL; + } + self->data->computeDijkstra(root_x, root_y, diagonal_cost); Py_RETURN_NONE; } -PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args) +PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { return NULL; } - + float distance = self->data->getDijkstraDistance(x, y); if (distance < 0) { Py_RETURN_NONE; // Invalid position } - + return PyFloat_FromDouble(distance); } -PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args) +PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) { int x, y; - if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + if (!PyPosition_ParseInt(args, kwds, &x, &y)) { return NULL; } - + std::vector> path = self->data->getDijkstraPath(x, y); - + PyObject* path_list = PyList_New(path.size()); for (size_t i = 0; i < path.size(); i++) { PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second); PyList_SetItem(path_list, i, pos); // Steals reference } - + return path_list; } PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - int x1, y1, x2, y2; + static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL}; + PyObject* start_obj = NULL; + PyObject* end_obj = NULL; float diagonal_cost = 1.41f; - - static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast(kwlist), - &x1, &y1, &x2, &y2, &diagonal_cost)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast(kwlist), + &start_obj, &end_obj, &diagonal_cost)) { return NULL; } - + + int x1, y1, x2, y2; + if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) { + return NULL; + } + if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) { + return NULL; + } + // Compute A* path std::vector> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost); - + // Convert to Python list PyObject* path_list = PyList_New(path.size()); for (size_t i = 0; i < path.size(); i++) { PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second); PyList_SetItem(path_list, i, pos); // Steals reference } - + return path_list; } @@ -1812,72 +1837,63 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) { PyMethodDef UIGrid::methods[] = { {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, - "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" "Compute field of view from a position.\n\n" "Args:\n" - " x: X coordinate of the viewer\n" - " y: Y coordinate of the viewer\n" + " pos: Position as (x, y) tuple, list, or Vector\n" " radius: Maximum view distance (0 = unlimited)\n" " light_walls: Whether walls are lit when visible\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" - "Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, - {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, - "is_in_fov(x: int, y: int) -> bool\n\n" + "Updates the internal FOV state. Use is_in_fov(pos) to query visibility."}, + {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS, + "is_in_fov(pos) -> bool\n\n" "Check if a cell is in the field of view.\n\n" "Args:\n" - " x: X coordinate to check\n" - " y: Y coordinate to check\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " True if the cell is visible, False otherwise\n\n" "Must call compute_fov() first to calculate visibility."}, - {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, - "find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, + "find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Find A* path between two points.\n\n" "Args:\n" - " x1: Starting X coordinate\n" - " y1: Starting Y coordinate\n" - " x2: Target X coordinate\n" - " y2: Target Y coordinate\n" + " start: Starting position as (x, y) tuple, list, or Vector\n" + " end: Target position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" "Uses A* algorithm with walkability from grid cells."}, - {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, - "compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n" + {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, + "compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n" "Compute Dijkstra map from root position.\n\n" "Args:\n" - " root_x: X coordinate of the root/target\n" - " root_y: Y coordinate of the root/target\n" + " root: Root position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Precomputes distances from all reachable cells to the root.\n" "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n" "Useful for multiple entities pathfinding to the same target."}, - {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, - "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS, + "get_dijkstra_distance(pos) -> Optional[float]\n\n" "Get distance from Dijkstra root to position.\n\n" "Args:\n" - " x: X coordinate to query\n" - " y: Y coordinate to query\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " Distance as float, or None if position is unreachable or invalid\n\n" "Must call compute_dijkstra() first."}, - {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, - "get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n" + {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS, + "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n" "Get path from position to Dijkstra root.\n\n" "Args:\n" - " x: Starting X coordinate\n" - " y: Starting Y coordinate\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " List of (x, y) tuples representing path to root, empty if unreachable\n\n" "Must call compute_dijkstra() first. Path includes start but not root position."}, {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, - "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + "compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Compute A* path between two points.\n\n" "Args:\n" - " x1: Starting X coordinate\n" - " y1: Starting Y coordinate\n" - " x2: Target X coordinate\n" - " y2: Target Y coordinate\n" + " start: Starting position as (x, y) tuple, list, or Vector\n" + " end: Target position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" @@ -1917,72 +1933,63 @@ PyMethodDef UIGrid_all_methods[] = { UIDRAWABLE_METHODS, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, - "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" "Compute field of view from a position.\n\n" "Args:\n" - " x: X coordinate of the viewer\n" - " y: Y coordinate of the viewer\n" + " pos: Position as (x, y) tuple, list, or Vector\n" " radius: Maximum view distance (0 = unlimited)\n" " light_walls: Whether walls are lit when visible\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" - "Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, - {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, - "is_in_fov(x: int, y: int) -> bool\n\n" + "Updates the internal FOV state. Use is_in_fov(pos) to query visibility."}, + {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS, + "is_in_fov(pos) -> bool\n\n" "Check if a cell is in the field of view.\n\n" "Args:\n" - " x: X coordinate to check\n" - " y: Y coordinate to check\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " True if the cell is visible, False otherwise\n\n" "Must call compute_fov() first to calculate visibility."}, - {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, - "find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + {"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS, + "find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Find A* path between two points.\n\n" "Args:\n" - " x1: Starting X coordinate\n" - " y1: Starting Y coordinate\n" - " x2: Target X coordinate\n" - " y2: Target Y coordinate\n" + " start: Starting position as (x, y) tuple, list, or Vector\n" + " end: Target position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" "Uses A* algorithm with walkability from grid cells."}, - {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, - "compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n" + {"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS, + "compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n" "Compute Dijkstra map from root position.\n\n" "Args:\n" - " root_x: X coordinate of the root/target\n" - " root_y: Y coordinate of the root/target\n" + " root: Root position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Precomputes distances from all reachable cells to the root.\n" "Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n" "Useful for multiple entities pathfinding to the same target."}, - {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, - "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS, + "get_dijkstra_distance(pos) -> Optional[float]\n\n" "Get distance from Dijkstra root to position.\n\n" "Args:\n" - " x: X coordinate to query\n" - " y: Y coordinate to query\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " Distance as float, or None if position is unreachable or invalid\n\n" "Must call compute_dijkstra() first."}, - {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS, - "get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n" + {"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS, + "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n" "Get path from position to Dijkstra root.\n\n" "Args:\n" - " x: Starting X coordinate\n" - " y: Starting Y coordinate\n\n" + " pos: Position as (x, y) tuple, list, or Vector\n\n" "Returns:\n" " List of (x, y) tuples representing path to root, empty if unreachable\n\n" "Must call compute_dijkstra() first. Path includes start but not root position."}, {"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS, - "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + "compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Compute A* path between two points.\n\n" "Args:\n" - " x1: Starting X coordinate\n" - " y1: Starting Y coordinate\n" - " x2: Target X coordinate\n" - " y2: Target Y coordinate\n" + " start: Starting position as (x, y) tuple, list, or Vector\n" + " end: Target position as (x, y) tuple, list, or Vector\n" " diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n" "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" @@ -2055,7 +2062,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, MCRF_PROPERTY(on_click, "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." + "Function receives (pos: Vector, button: str, action: str)." ), (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 @@ -2083,11 +2090,11 @@ PyGetSetDef UIGrid::getsetters[] = { UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID), // #142 - Grid cell mouse events {"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter, - "Callback when mouse enters a grid cell. Called with (cell_x, cell_y).", NULL}, + "Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL}, {"on_cell_exit", (getter)UIGrid::get_on_cell_exit, (setter)UIGrid::set_on_cell_exit, - "Callback when mouse exits a grid cell. Called with (cell_x, cell_y).", NULL}, + "Callback when mouse exits a grid cell. Called with (cell_pos: Vector).", NULL}, {"on_cell_click", (getter)UIGrid::get_on_cell_click, (setter)UIGrid::set_on_cell_click, - "Callback when a grid cell is clicked. Called with (cell_x, cell_y).", NULL}, + "Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL}, {"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL, "Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL}, {NULL} /* Sentinel */ @@ -2249,29 +2256,47 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) { if (new_cell != hovered_cell) { // Fire exit callback for old cell if (hovered_cell.has_value() && on_cell_exit_callable) { - PyObject* args = Py_BuildValue("(ii)", hovered_cell->x, hovered_cell->y); - PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args); - Py_DECREF(args); - if (!result) { - std::cerr << "Cell exit callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else { - Py_DECREF(result); + // Create Vector object for cell position - must fetch finalized type from module + PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)hovered_cell->x, (float)hovered_cell->y); + Py_DECREF(vector_type); + if (cell_pos) { + PyObject* args = Py_BuildValue("(O)", cell_pos); + Py_DECREF(cell_pos); + PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args); + Py_DECREF(args); + if (!result) { + std::cerr << "Cell exit callback raised an exception:" << std::endl; + PyErr_Print(); + PyErr_Clear(); + } else { + Py_DECREF(result); + } + } } } // Fire enter callback for new cell if (new_cell.has_value() && on_cell_enter_callable) { - PyObject* args = Py_BuildValue("(ii)", new_cell->x, new_cell->y); - PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args); - Py_DECREF(args); - if (!result) { - std::cerr << "Cell enter callback raised an exception:" << std::endl; - PyErr_Print(); - PyErr_Clear(); - } else { - Py_DECREF(result); + // Create Vector object for cell position - must fetch finalized type from module + PyObject* vector_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); + if (vector_type) { + PyObject* cell_pos = PyObject_CallFunction(vector_type, "ff", (float)new_cell->x, (float)new_cell->y); + Py_DECREF(vector_type); + if (cell_pos) { + PyObject* args = Py_BuildValue("(O)", cell_pos); + Py_DECREF(cell_pos); + PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args); + Py_DECREF(args); + if (!result) { + std::cerr << "Cell enter callback raised an exception:" << std::endl; + PyErr_Print(); + PyErr_Clear(); + } else { + Py_DECREF(result); + } + } } } diff --git a/src/UIGrid.h b/src/UIGrid.h index 62bb1b0..ef29e6d 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -165,11 +165,11 @@ public: static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure); static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds); - static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args); + static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds); - static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args); - static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args); + static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds); + static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115 static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169 diff --git a/src/UILine.cpp b/src/UILine.cpp index a08429c..6312c16 100644 --- a/src/UILine.cpp +++ b/src/UILine.cpp @@ -451,7 +451,7 @@ PyGetSetDef UILine::getsetters[] = { {"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness, MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL}, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, - MCRF_PROPERTY(on_click, "Callable executed when line is clicked."), + MCRF_PROPERTY(on_click, "Callable executed when line is clicked. Function receives (pos: Vector, button: str, action: str)."), (void*)PyObjectsEnum::UILINE}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."), diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 638453c..a77fea3 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -343,7 +343,7 @@ PyGetSetDef UISprite::getsetters[] = { {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, MCRF_PROPERTY(on_click, "Callable executed when object is clicked. " - "Function receives (x, y) coordinates of click." + "Function receives (pos: Vector, button: str, action: str)." ), (void*)PyObjectsEnum::UISPRITE}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, MCRF_PROPERTY(z_index, diff --git a/tests/test_callback_vector.py b/tests/test_callback_vector.py new file mode 100644 index 0000000..f81aa6b --- /dev/null +++ b/tests/test_callback_vector.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Test that callbacks return Vector objects instead of separate x, y values.""" + +import sys +import mcrfpy + +# Track test results +results = [] + +def test_click_callback_signature(pos, button, action): + """Test on_click callback receives Vector.""" + # Check if pos is a Vector + if isinstance(pos, mcrfpy.Vector): + results.append(("on_click pos is Vector", True)) + print(f"PASS: on_click receives Vector: {pos}") + else: + results.append(("on_click pos is Vector", False)) + print(f"FAIL: on_click receives {type(pos).__name__} instead of Vector: {pos}") + + # Verify button and action are strings + if isinstance(button, str) and isinstance(action, str): + results.append(("on_click button/action are strings", True)) + print(f"PASS: button={button!r}, action={action!r}") + else: + results.append(("on_click button/action are strings", False)) + print(f"FAIL: button={type(button).__name__}, action={type(action).__name__}") + +def test_on_enter_callback_signature(pos, button, action): + """Test on_enter callback receives Vector.""" + if isinstance(pos, mcrfpy.Vector): + results.append(("on_enter pos is Vector", True)) + print(f"PASS: on_enter receives Vector: {pos}") + else: + results.append(("on_enter pos is Vector", False)) + print(f"FAIL: on_enter receives {type(pos).__name__} instead of Vector") + +def test_on_exit_callback_signature(pos, button, action): + """Test on_exit callback receives Vector.""" + if isinstance(pos, mcrfpy.Vector): + results.append(("on_exit pos is Vector", True)) + print(f"PASS: on_exit receives Vector: {pos}") + else: + results.append(("on_exit pos is Vector", False)) + print(f"FAIL: on_exit receives {type(pos).__name__} instead of Vector") + +def test_on_move_callback_signature(pos, button, action): + """Test on_move callback receives Vector.""" + if isinstance(pos, mcrfpy.Vector): + results.append(("on_move pos is Vector", True)) + print(f"PASS: on_move receives Vector: {pos}") + else: + results.append(("on_move pos is Vector", False)) + print(f"FAIL: on_move receives {type(pos).__name__} instead of Vector") + +def test_cell_click_callback_signature(cell_pos): + """Test on_cell_click callback receives Vector.""" + if isinstance(cell_pos, mcrfpy.Vector): + results.append(("on_cell_click pos is Vector", True)) + print(f"PASS: on_cell_click receives Vector: {cell_pos}") + else: + results.append(("on_cell_click pos is Vector", False)) + print(f"FAIL: on_cell_click receives {type(cell_pos).__name__} instead of Vector") + +def test_cell_enter_callback_signature(cell_pos): + """Test on_cell_enter callback receives Vector.""" + if isinstance(cell_pos, mcrfpy.Vector): + results.append(("on_cell_enter pos is Vector", True)) + print(f"PASS: on_cell_enter receives Vector: {cell_pos}") + else: + results.append(("on_cell_enter pos is Vector", False)) + print(f"FAIL: on_cell_enter receives {type(cell_pos).__name__} instead of Vector") + +def test_cell_exit_callback_signature(cell_pos): + """Test on_cell_exit callback receives Vector.""" + if isinstance(cell_pos, mcrfpy.Vector): + results.append(("on_cell_exit pos is Vector", True)) + print(f"PASS: on_cell_exit receives Vector: {cell_pos}") + else: + results.append(("on_cell_exit pos is Vector", False)) + print(f"FAIL: on_cell_exit receives {type(cell_pos).__name__} instead of Vector") + +def run_test(runtime): + """Set up test and simulate interactions.""" + print("=" * 50) + print("Testing callback Vector return values") + print("=" * 50) + + # Create a test scene + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + + # Create a Frame with callbacks + frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200)) + frame.on_click = test_click_callback_signature + frame.on_enter = test_on_enter_callback_signature + frame.on_exit = test_on_exit_callback_signature + frame.on_move = test_on_move_callback_signature + ui.append(frame) + + # Create a Grid with cell callbacks + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + grid = mcrfpy.Grid(pos=(350, 100), size=(200, 200), grid_size=(10, 10), texture=texture) + grid.on_cell_click = test_cell_click_callback_signature + grid.on_cell_enter = test_cell_enter_callback_signature + grid.on_cell_exit = test_cell_exit_callback_signature + ui.append(grid) + + mcrfpy.setScene("test") + + print("\n--- Test Setup Complete ---") + print("To test interactively:") + print(" - Click on the Frame (left side) to test on_click") + print(" - Move mouse over Frame to test on_enter/on_exit/on_move") + print(" - Click on the Grid (right side) to test on_cell_click") + print(" - Move mouse over Grid to test on_cell_enter/on_cell_exit") + print("\nPress Escape to exit.") + + # For headless testing, simulate a callback call directly + print("\n--- Simulating callback calls ---") + + # Test that the callbacks are set up correctly + test_click_callback_signature(mcrfpy.Vector(150, 150), "left", "start") + test_on_enter_callback_signature(mcrfpy.Vector(100, 100), "enter", "start") + test_on_exit_callback_signature(mcrfpy.Vector(300, 300), "exit", "start") + test_on_move_callback_signature(mcrfpy.Vector(125, 175), "move", "start") + test_cell_click_callback_signature(mcrfpy.Vector(5, 3)) + test_cell_enter_callback_signature(mcrfpy.Vector(2, 7)) + test_cell_exit_callback_signature(mcrfpy.Vector(8, 1)) + + # Print summary + print("\n" + "=" * 50) + print("SUMMARY") + print("=" * 50) + passed = sum(1 for _, success in results if success) + failed = sum(1 for _, success in results if not success) + print(f"Passed: {passed}") + print(f"Failed: {failed}") + + if failed == 0: + print("\nAll tests PASSED!") + sys.exit(0) + else: + print("\nSome tests FAILED!") + for name, success in results: + if not success: + print(f" FAILED: {name}") + sys.exit(1) + +# Run the test +mcrfpy.setTimer("test", run_test, 100) diff --git a/tests/unit/automation_vector_test.py b/tests/unit/automation_vector_test.py new file mode 100644 index 0000000..9791dfd --- /dev/null +++ b/tests/unit/automation_vector_test.py @@ -0,0 +1,152 @@ +"""Test automation module with new position parsing and Vector returns""" +import mcrfpy +from mcrfpy import automation +import sys + +# Track test results +passed = 0 +failed = 0 + +def test(name, condition): + global passed, failed + if condition: + print(f" PASS: {name}") + passed += 1 + else: + print(f" FAIL: {name}") + failed += 1 + +print("Testing automation module updates...") +print() + +# Test 1: position() returns Vector +print("1. Testing position() returns Vector...") +pos = automation.position() +test("position() returns Vector type", type(pos).__name__ == "Vector") +test("position has x attribute", hasattr(pos, 'x')) +test("position has y attribute", hasattr(pos, 'y')) +print() + +# Test 2: size() returns Vector +print("2. Testing size() returns Vector...") +sz = automation.size() +test("size() returns Vector type", type(sz).__name__ == "Vector") +test("size has x attribute", hasattr(sz, 'x')) +test("size has y attribute", hasattr(sz, 'y')) +test("size.x > 0", sz.x > 0) +test("size.y > 0", sz.y > 0) +print() + +# Test 3: onScreen() accepts various position formats +print("3. Testing onScreen() with various position formats...") +# Move mouse to a known position first +automation.moveTo((100, 100)) +test("onScreen((100, 100)) with tuple", automation.onScreen((100, 100)) == True) +test("onScreen([50, 50]) with list", automation.onScreen([50, 50]) == True) +test("onScreen(mcrfpy.Vector(200, 200)) with Vector", automation.onScreen(mcrfpy.Vector(200, 200)) == True) +# Should be off-screen (negative) +test("onScreen((-10, -10)) returns False", automation.onScreen((-10, -10)) == False) +print() + +# Test 4: moveTo() accepts position as grouped argument +print("4. Testing moveTo() with grouped position...") +automation.moveTo((150, 150)) +pos = automation.position() +test("moveTo((150, 150)) moves to correct x", int(pos.x) == 150) +test("moveTo((150, 150)) moves to correct y", int(pos.y) == 150) + +automation.moveTo([200, 200]) +pos = automation.position() +test("moveTo([200, 200]) with list", int(pos.x) == 200 and int(pos.y) == 200) + +automation.moveTo(mcrfpy.Vector(250, 250)) +pos = automation.position() +test("moveTo(Vector(250, 250)) with Vector", int(pos.x) == 250 and int(pos.y) == 250) +print() + +# Test 5: moveRel() accepts offset as grouped argument +print("5. Testing moveRel() with grouped offset...") +automation.moveTo((100, 100)) # Start position +automation.moveRel((50, 50)) # Relative move +pos = automation.position() +test("moveRel((50, 50)) from (100, 100)", int(pos.x) == 150 and int(pos.y) == 150) +print() + +# Test 6: click() accepts optional position as grouped argument +print("6. Testing click() with grouped position...") +# Click at current position (no args should work) +try: + automation.click() + test("click() with no args (current position)", True) +except: + test("click() with no args (current position)", False) + +try: + automation.click((200, 200)) + test("click((200, 200)) with tuple", True) +except: + test("click((200, 200)) with tuple", False) + +try: + automation.click([300, 300], clicks=2) + test("click([300, 300], clicks=2) with list", True) +except: + test("click([300, 300], clicks=2) with list", False) +print() + +# Test 7: scroll() accepts position as second grouped argument +print("7. Testing scroll() with grouped position...") +try: + automation.scroll(3) # No position - use current + test("scroll(3) without position", True) +except: + test("scroll(3) without position", False) + +try: + automation.scroll(3, (100, 100)) + test("scroll(3, (100, 100)) with tuple", True) +except: + test("scroll(3, (100, 100)) with tuple", False) +print() + +# Test 8: mouseDown/mouseUp with grouped position +print("8. Testing mouseDown/mouseUp with grouped position...") +try: + automation.mouseDown((100, 100)) + automation.mouseUp((100, 100)) + test("mouseDown/mouseUp((100, 100)) with tuple", True) +except: + test("mouseDown/mouseUp((100, 100)) with tuple", False) +print() + +# Test 9: dragTo() with grouped position +print("9. Testing dragTo() with grouped position...") +automation.moveTo((100, 100)) +try: + automation.dragTo((200, 200)) + test("dragTo((200, 200)) with tuple", True) +except Exception as e: + print(f" Error: {e}") + test("dragTo((200, 200)) with tuple", False) +print() + +# Test 10: dragRel() with grouped offset +print("10. Testing dragRel() with grouped offset...") +automation.moveTo((100, 100)) +try: + automation.dragRel((50, 50)) + test("dragRel((50, 50)) with tuple", True) +except Exception as e: + print(f" Error: {e}") + test("dragRel((50, 50)) with tuple", False) +print() + +# Summary +print("=" * 40) +print(f"Results: {passed} passed, {failed} failed") +if failed == 0: + print("All tests passed!") + sys.exit(0) +else: + print("Some tests failed") + sys.exit(1) diff --git a/tests/unit/test_drawable_move_resize_position_parsing.py b/tests/unit/test_drawable_move_resize_position_parsing.py new file mode 100644 index 0000000..ae2dffd --- /dev/null +++ b/tests/unit/test_drawable_move_resize_position_parsing.py @@ -0,0 +1,129 @@ +"""Test that Drawable.move() and Drawable.resize() accept flexible position arguments.""" +import mcrfpy +import sys + +def run_tests(): + """Test the new position parsing for move() and resize().""" + errors = [] + + # Create a test scene + scene = mcrfpy.Scene("test_drawable_methods") + + # Create a Frame to test with (since Drawable is abstract) + frame = mcrfpy.Frame(pos=(100, 100), size=(50, 50)) + scene.children.append(frame) + + # Test 1: move() with two separate arguments (original behavior) + try: + frame.x = 100 + frame.y = 100 + frame.move(10, 20) + if not (frame.x == 110 and frame.y == 120): + errors.append(f"move(10, 20) failed: got ({frame.x}, {frame.y}), expected (110, 120)") + else: + print("PASS: move(dx, dy) with two arguments works") + except Exception as e: + errors.append(f"move(10, 20) raised: {e}") + + # Test 2: move() with a tuple + try: + frame.x = 100 + frame.y = 100 + frame.move((15, 25)) + if not (frame.x == 115 and frame.y == 125): + errors.append(f"move((15, 25)) failed: got ({frame.x}, {frame.y}), expected (115, 125)") + else: + print("PASS: move((dx, dy)) with tuple works") + except Exception as e: + errors.append(f"move((15, 25)) raised: {e}") + + # Test 3: move() with a list + try: + frame.x = 100 + frame.y = 100 + frame.move([5, 10]) + if not (frame.x == 105 and frame.y == 110): + errors.append(f"move([5, 10]) failed: got ({frame.x}, {frame.y}), expected (105, 110)") + else: + print("PASS: move([dx, dy]) with list works") + except Exception as e: + errors.append(f"move([5, 10]) raised: {e}") + + # Test 4: move() with a Vector + try: + frame.x = 100 + frame.y = 100 + vec = mcrfpy.Vector(12, 18) + frame.move(vec) + if not (frame.x == 112 and frame.y == 118): + errors.append(f"move(Vector(12, 18)) failed: got ({frame.x}, {frame.y}), expected (112, 118)") + else: + print("PASS: move(Vector) works") + except Exception as e: + errors.append(f"move(Vector) raised: {e}") + + # Test 5: resize() with two separate arguments (original behavior) + try: + frame.resize(200, 150) + if not (frame.w == 200 and frame.h == 150): + errors.append(f"resize(200, 150) failed: got ({frame.w}, {frame.h}), expected (200, 150)") + else: + print("PASS: resize(w, h) with two arguments works") + except Exception as e: + errors.append(f"resize(200, 150) raised: {e}") + + # Test 6: resize() with a tuple + try: + frame.resize((180, 120)) + if not (frame.w == 180 and frame.h == 120): + errors.append(f"resize((180, 120)) failed: got ({frame.w}, {frame.h}), expected (180, 120)") + else: + print("PASS: resize((w, h)) with tuple works") + except Exception as e: + errors.append(f"resize((180, 120)) raised: {e}") + + # Test 7: resize() with a list + try: + frame.resize([100, 80]) + if not (frame.w == 100 and frame.h == 80): + errors.append(f"resize([100, 80]) failed: got ({frame.w}, {frame.h}), expected (100, 80)") + else: + print("PASS: resize([w, h]) with list works") + except Exception as e: + errors.append(f"resize([100, 80]) raised: {e}") + + # Test 8: resize() with a Vector + try: + vec = mcrfpy.Vector(250, 200) + frame.resize(vec) + if not (frame.w == 250 and frame.h == 200): + errors.append(f"resize(Vector(250, 200)) failed: got ({frame.w}, {frame.h}), expected (250, 200)") + else: + print("PASS: resize(Vector) works") + except Exception as e: + errors.append(f"resize(Vector) raised: {e}") + + # Test 9: move() with keyword argument pos + try: + frame.x = 100 + frame.y = 100 + frame.move(pos=(7, 13)) + if not (frame.x == 107 and frame.y == 113): + errors.append(f"move(pos=(7, 13)) failed: got ({frame.x}, {frame.y}), expected (107, 113)") + else: + print("PASS: move(pos=(dx, dy)) with keyword works") + except Exception as e: + errors.append(f"move(pos=(7, 13)) raised: {e}") + + # Summary + if errors: + print("\nFAILURES:") + for e in errors: + print(f" - {e}") + sys.exit(1) + else: + print("\nAll tests passed!") + sys.exit(0) + +# Run tests +run_tests() diff --git a/tests/unit/test_entity_position_parsing.py b/tests/unit/test_entity_position_parsing.py new file mode 100644 index 0000000..716c178 --- /dev/null +++ b/tests/unit/test_entity_position_parsing.py @@ -0,0 +1,135 @@ +"""Test Entity.at() and Entity.path_to() position argument parsing. + +These methods should accept: +- Two separate integers: method(x, y) +- A tuple: method((x, y)) +- Keyword arguments: method(x=x, y=y) or method(pos=(x, y)) +- A Vector: method(Vector(x, y)) +""" +import mcrfpy +import sys + +def run_tests(): + # Create a grid with some walkable cells + grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(320, 320)) + + # Make the grid walkable + for x in range(10): + for y in range(10): + grid.at(x, y).walkable = True + + # Create an entity at (2, 2) + entity = mcrfpy.Entity(grid_pos=(2, 2), grid=grid) + + print("Testing Entity.at() position parsing...") + + # Test 1: Two separate integers + try: + state1 = entity.at(3, 3) + print(" PASS: entity.at(3, 3)") + except Exception as e: + print(f" FAIL: entity.at(3, 3) - {e}") + return False + + # Test 2: Tuple argument + try: + state2 = entity.at((4, 4)) + print(" PASS: entity.at((4, 4))") + except Exception as e: + print(f" FAIL: entity.at((4, 4)) - {e}") + return False + + # Test 3: Keyword arguments + try: + state3 = entity.at(x=5, y=5) + print(" PASS: entity.at(x=5, y=5)") + except Exception as e: + print(f" FAIL: entity.at(x=5, y=5) - {e}") + return False + + # Test 4: pos= keyword argument + try: + state4 = entity.at(pos=(6, 6)) + print(" PASS: entity.at(pos=(6, 6))") + except Exception as e: + print(f" FAIL: entity.at(pos=(6, 6)) - {e}") + return False + + # Test 5: List argument + try: + state5 = entity.at([7, 7]) + print(" PASS: entity.at([7, 7])") + except Exception as e: + print(f" FAIL: entity.at([7, 7]) - {e}") + return False + + # Test 6: Vector argument + try: + vec = mcrfpy.Vector(8, 8) + state6 = entity.at(vec) + print(" PASS: entity.at(Vector(8, 8))") + except Exception as e: + print(f" FAIL: entity.at(Vector(8, 8)) - {e}") + return False + + print("\nTesting Entity.path_to() position parsing...") + + # Test 1: Two separate integers + try: + path1 = entity.path_to(5, 5) + print(" PASS: entity.path_to(5, 5)") + except Exception as e: + print(f" FAIL: entity.path_to(5, 5) - {e}") + return False + + # Test 2: Tuple argument + try: + path2 = entity.path_to((6, 6)) + print(" PASS: entity.path_to((6, 6))") + except Exception as e: + print(f" FAIL: entity.path_to((6, 6)) - {e}") + return False + + # Test 3: Keyword arguments + try: + path3 = entity.path_to(x=7, y=7) + print(" PASS: entity.path_to(x=7, y=7)") + except Exception as e: + print(f" FAIL: entity.path_to(x=7, y=7) - {e}") + return False + + # Test 4: pos= keyword argument + try: + path4 = entity.path_to(pos=(8, 8)) + print(" PASS: entity.path_to(pos=(8, 8))") + except Exception as e: + print(f" FAIL: entity.path_to(pos=(8, 8)) - {e}") + return False + + # Test 5: List argument + try: + path5 = entity.path_to([9, 9]) + print(" PASS: entity.path_to([9, 9])") + except Exception as e: + print(f" FAIL: entity.path_to([9, 9]) - {e}") + return False + + # Test 6: Vector argument + try: + vec = mcrfpy.Vector(4, 4) + path6 = entity.path_to(vec) + print(" PASS: entity.path_to(Vector(4, 4))") + except Exception as e: + print(f" FAIL: entity.path_to(Vector(4, 4)) - {e}") + return False + + print("\nAll tests passed!") + return True + +# Run tests immediately (no game loop needed for these) +if run_tests(): + print("PASS") + sys.exit(0) +else: + print("FAIL") + sys.exit(1) diff --git a/tests/unit/test_grid_pathfinding_positions.py b/tests/unit/test_grid_pathfinding_positions.py new file mode 100644 index 0000000..ec942b6 --- /dev/null +++ b/tests/unit/test_grid_pathfinding_positions.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Test Grid pathfinding methods with new position parsing. + +Tests that Grid.find_path, Grid.compute_fov, etc. accept positions +in multiple formats: tuples, lists, Vectors. +""" +import mcrfpy +import sys + +def run_tests(): + """Run all grid pathfinding position parsing tests.""" + print("Testing Grid pathfinding position parsing...") + + # Create a test grid + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + grid = mcrfpy.Grid(grid_size=(10, 10), texture=texture, pos=(0, 0), size=(320, 320)) + + # Set up walkability: all cells walkable initially + for y in range(10): + for x in range(10): + cell = grid.at((x, y)) + cell.walkable = True + + # Add a wall in the middle + grid.at((5, 5)).walkable = False + + print(" Grid created with walkable cells and one wall at (5,5)") + + # ============ Test find_path ============ + print("\n Testing find_path...") + + # Test with tuple positions + path1 = grid.find_path((0, 0), (3, 3)) + assert path1 is not None, "find_path with tuples returned None" + assert len(path1) > 0, "find_path with tuples returned empty path" + print(f" find_path((0,0), (3,3)) -> {len(path1)} steps: PASS") + + # Test with list positions + path2 = grid.find_path([0, 0], [3, 3]) + assert path2 is not None, "find_path with lists returned None" + assert len(path2) > 0, "find_path with lists returned empty path" + print(f" find_path([0,0], [3,3]) -> {len(path2)} steps: PASS") + + # Test with Vector positions + start_vec = mcrfpy.Vector(0, 0) + end_vec = mcrfpy.Vector(3, 3) + path3 = grid.find_path(start_vec, end_vec) + assert path3 is not None, "find_path with Vectors returned None" + assert len(path3) > 0, "find_path with Vectors returned empty path" + print(f" find_path(Vector(0,0), Vector(3,3)) -> {len(path3)} steps: PASS") + + # Test path with diagonal_cost parameter + path4 = grid.find_path((0, 0), (3, 3), diagonal_cost=1.41) + assert path4 is not None, "find_path with diagonal_cost returned None" + print(f" find_path with diagonal_cost=1.41: PASS") + + # ============ Test compute_fov / is_in_fov ============ + print("\n Testing compute_fov / is_in_fov...") + + # All cells transparent for FOV testing + for y in range(10): + for x in range(10): + cell = grid.at((x, y)) + cell.transparent = True + + # Test compute_fov with tuple + grid.compute_fov((5, 5), radius=5) + print(" compute_fov((5,5), radius=5): PASS") + + # Test is_in_fov with tuple + in_fov1 = grid.is_in_fov((5, 5)) + assert in_fov1 == True, "Center should be in FOV" + print(f" is_in_fov((5,5)) = {in_fov1}: PASS") + + # Test is_in_fov with list + in_fov2 = grid.is_in_fov([4, 5]) + assert in_fov2 == True, "Adjacent cell should be in FOV" + print(f" is_in_fov([4,5]) = {in_fov2}: PASS") + + # Test is_in_fov with Vector + pos_vec = mcrfpy.Vector(6, 5) + in_fov3 = grid.is_in_fov(pos_vec) + assert in_fov3 == True, "Adjacent cell should be in FOV" + print(f" is_in_fov(Vector(6,5)) = {in_fov3}: PASS") + + # Test compute_fov with Vector + center_vec = mcrfpy.Vector(3, 3) + grid.compute_fov(center_vec, radius=3) + print(" compute_fov(Vector(3,3), radius=3): PASS") + + # ============ Test compute_dijkstra / get_dijkstra_* ============ + print("\n Testing Dijkstra methods...") + + # Test compute_dijkstra with tuple + grid.compute_dijkstra((0, 0)) + print(" compute_dijkstra((0,0)): PASS") + + # Test get_dijkstra_distance with tuple + dist1 = grid.get_dijkstra_distance((3, 3)) + assert dist1 is not None, "Distance should not be None for reachable cell" + print(f" get_dijkstra_distance((3,3)) = {dist1:.2f}: PASS") + + # Test get_dijkstra_distance with list + dist2 = grid.get_dijkstra_distance([2, 2]) + assert dist2 is not None, "Distance should not be None for reachable cell" + print(f" get_dijkstra_distance([2,2]) = {dist2:.2f}: PASS") + + # Test get_dijkstra_distance with Vector + dist3 = grid.get_dijkstra_distance(mcrfpy.Vector(1, 1)) + assert dist3 is not None, "Distance should not be None for reachable cell" + print(f" get_dijkstra_distance(Vector(1,1)) = {dist3:.2f}: PASS") + + # Test get_dijkstra_path with tuple + dpath1 = grid.get_dijkstra_path((3, 3)) + assert dpath1 is not None, "Dijkstra path should not be None" + print(f" get_dijkstra_path((3,3)) -> {len(dpath1)} steps: PASS") + + # Test get_dijkstra_path with Vector + dpath2 = grid.get_dijkstra_path(mcrfpy.Vector(4, 4)) + assert dpath2 is not None, "Dijkstra path should not be None" + print(f" get_dijkstra_path(Vector(4,4)) -> {len(dpath2)} steps: PASS") + + # ============ Test compute_astar_path ============ + print("\n Testing compute_astar_path...") + + # Test with tuples + apath1 = grid.compute_astar_path((0, 0), (3, 3)) + assert apath1 is not None, "A* path should not be None" + print(f" compute_astar_path((0,0), (3,3)) -> {len(apath1)} steps: PASS") + + # Test with lists + apath2 = grid.compute_astar_path([1, 1], [4, 4]) + assert apath2 is not None, "A* path should not be None" + print(f" compute_astar_path([1,1], [4,4]) -> {len(apath2)} steps: PASS") + + # Test with Vectors + apath3 = grid.compute_astar_path(mcrfpy.Vector(2, 2), mcrfpy.Vector(7, 7)) + assert apath3 is not None, "A* path should not be None" + print(f" compute_astar_path(Vector(2,2), Vector(7,7)) -> {len(apath3)} steps: PASS") + + print("\n" + "="*50) + print("All grid pathfinding position tests PASSED!") + print("="*50) + return True + +# Run tests +try: + success = run_tests() + if success: + print("\nPASS") + sys.exit(0) +except Exception as e: + print(f"\nFAIL: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/unit/test_layer_position_parsing.py b/tests/unit/test_layer_position_parsing.py new file mode 100644 index 0000000..f097a5e --- /dev/null +++ b/tests/unit/test_layer_position_parsing.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Test ColorLayer and TileLayer position parsing with new PyPositionHelper pattern.""" +import sys +import mcrfpy + +def test_colorlayer_at(): + """Test ColorLayer.at() with various position formats.""" + print("Testing ColorLayer.at() position parsing...") + + # Create a grid and color layer + grid = mcrfpy.Grid(grid_size=(10, 10)) + layer = mcrfpy.ColorLayer(z_index=-1, grid_size=(10, 10)) + grid.layers.append(layer) + + # Set a color at position + layer.set((5, 5), mcrfpy.Color(255, 0, 0)) + + # Test at() with tuple + c1 = layer.at((5, 5)) + assert c1.r == 255 and c1.g == 0 and c1.b == 0, f"Failed: tuple position - got {c1.r},{c1.g},{c1.b}" + print(" - tuple position: PASS") + + # Test at() with two args + c2 = layer.at(5, 5) + assert c2.r == 255 and c2.g == 0 and c2.b == 0, f"Failed: two args - got {c2.r},{c2.g},{c2.b}" + print(" - two args: PASS") + + # Test at() with list (if supported) + c3 = layer.at([5, 5]) + assert c3.r == 255 and c3.g == 0 and c3.b == 0, f"Failed: list position - got {c3.r},{c3.g},{c3.b}" + print(" - list position: PASS") + + # Test at() with Vector + vec = mcrfpy.Vector(5, 5) + c4 = layer.at(vec) + assert c4.r == 255 and c4.g == 0 and c4.b == 0, f"Failed: Vector position - got {c4.r},{c4.g},{c4.b}" + print(" - Vector position: PASS") + + print("ColorLayer.at(): ALL PASS") + + +def test_colorlayer_set(): + """Test ColorLayer.set() with grouped position.""" + print("Testing ColorLayer.set() grouped position...") + + grid = mcrfpy.Grid(grid_size=(10, 10)) + layer = mcrfpy.ColorLayer(z_index=-1, grid_size=(10, 10)) + grid.layers.append(layer) + + # Test set() with tuple position + layer.set((3, 4), mcrfpy.Color(0, 255, 0)) + c = layer.at((3, 4)) + assert c.g == 255, f"Failed: tuple position - got g={c.g}" + print(" - tuple position: PASS") + + # Test set() with list position + layer.set([7, 8], (0, 0, 255)) # Also test tuple color + c2 = layer.at((7, 8)) + assert c2.b == 255, f"Failed: list position - got b={c2.b}" + print(" - list position: PASS") + + # Test set() with Vector position + layer.set(mcrfpy.Vector(1, 1), mcrfpy.Color(128, 128, 128)) + c3 = layer.at((1, 1)) + assert c3.r == 128, f"Failed: Vector position - got r={c3.r}" + print(" - Vector position: PASS") + + print("ColorLayer.set(): ALL PASS") + + +def test_tilelayer_at(): + """Test TileLayer.at() with various position formats.""" + print("Testing TileLayer.at() position parsing...") + + # Create a grid and tile layer + grid = mcrfpy.Grid(grid_size=(10, 10)) + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + layer = mcrfpy.TileLayer(z_index=-1, texture=texture, grid_size=(10, 10)) + grid.layers.append(layer) + + # Set a tile at position + layer.set((5, 5), 42) + + # Test at() with tuple + t1 = layer.at((5, 5)) + assert t1 == 42, f"Failed: tuple position - got {t1}" + print(" - tuple position: PASS") + + # Test at() with two args + t2 = layer.at(5, 5) + assert t2 == 42, f"Failed: two args - got {t2}" + print(" - two args: PASS") + + # Test at() with list + t3 = layer.at([5, 5]) + assert t3 == 42, f"Failed: list position - got {t3}" + print(" - list position: PASS") + + # Test at() with Vector + t4 = layer.at(mcrfpy.Vector(5, 5)) + assert t4 == 42, f"Failed: Vector position - got {t4}" + print(" - Vector position: PASS") + + print("TileLayer.at(): ALL PASS") + + +def test_tilelayer_set(): + """Test TileLayer.set() with grouped position.""" + print("Testing TileLayer.set() grouped position...") + + grid = mcrfpy.Grid(grid_size=(10, 10)) + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + layer = mcrfpy.TileLayer(z_index=-1, texture=texture, grid_size=(10, 10)) + grid.layers.append(layer) + + # Test set() with tuple position + layer.set((3, 4), 10) + assert layer.at((3, 4)) == 10, "Failed: tuple position" + print(" - tuple position: PASS") + + # Test set() with list position + layer.set([7, 8], 20) + assert layer.at((7, 8)) == 20, "Failed: list position" + print(" - list position: PASS") + + # Test set() with Vector position + layer.set(mcrfpy.Vector(1, 1), 30) + assert layer.at((1, 1)) == 30, "Failed: Vector position" + print(" - Vector position: PASS") + + print("TileLayer.set(): ALL PASS") + + +# Run all tests +try: + test_colorlayer_at() + test_colorlayer_set() + test_tilelayer_at() + test_tilelayer_set() + print("\n=== ALL TESTS PASSED ===") + sys.exit(0) +except AssertionError as e: + print(f"\nTEST FAILED: {e}") + sys.exit(1) +except Exception as e: + print(f"\nTEST ERROR: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/tests/unit/test_position_helper.py b/tests/unit/test_position_helper.py new file mode 100644 index 0000000..ab4b335 --- /dev/null +++ b/tests/unit/test_position_helper.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Test script for PyPositionHelper - validates Grid.at() position parsing. + +This tests the standardized position argument parsing that supports: +- Two separate args: func(x, y) +- A tuple: func((x, y)) +- A list: func([x, y]) +- A Vector object: func(Vector(x, y)) +- Keyword args: func(x=x, y=y) or func(pos=(x,y)) +""" + +import sys +import mcrfpy + +def test_grid_at_position_parsing(): + """Test all the different ways to call Grid.at() with positions.""" + + # Create a test scene and grid + scene = mcrfpy.Scene("test_position") + + # Create a grid with enough cells to test indexing + grid = mcrfpy.Grid(grid_x=10, grid_y=10) + + errors = [] + + # Test 1: Two separate integer arguments + try: + point1 = grid.at(3, 4) + if point1 is None: + errors.append("Test 1 FAIL: grid.at(3, 4) returned None") + else: + print("Test 1 PASS: grid.at(3, 4) works") + except Exception as e: + errors.append(f"Test 1 FAIL: grid.at(3, 4) raised {type(e).__name__}: {e}") + + # Test 2: Tuple argument + try: + point2 = grid.at((5, 6)) + if point2 is None: + errors.append("Test 2 FAIL: grid.at((5, 6)) returned None") + else: + print("Test 2 PASS: grid.at((5, 6)) works") + except Exception as e: + errors.append(f"Test 2 FAIL: grid.at((5, 6)) raised {type(e).__name__}: {e}") + + # Test 3: List argument + try: + point3 = grid.at([7, 8]) + if point3 is None: + errors.append("Test 3 FAIL: grid.at([7, 8]) returned None") + else: + print("Test 3 PASS: grid.at([7, 8]) works") + except Exception as e: + errors.append(f"Test 3 FAIL: grid.at([7, 8]) raised {type(e).__name__}: {e}") + + # Test 4: Vector argument + try: + vec = mcrfpy.Vector(2, 3) + point4 = grid.at(vec) + if point4 is None: + errors.append("Test 4 FAIL: grid.at(Vector(2, 3)) returned None") + else: + print("Test 4 PASS: grid.at(Vector(2, 3)) works") + except Exception as e: + errors.append(f"Test 4 FAIL: grid.at(Vector(2, 3)) raised {type(e).__name__}: {e}") + + # Test 5: Keyword arguments x=, y= + try: + point5 = grid.at(x=1, y=2) + if point5 is None: + errors.append("Test 5 FAIL: grid.at(x=1, y=2) returned None") + else: + print("Test 5 PASS: grid.at(x=1, y=2) works") + except Exception as e: + errors.append(f"Test 5 FAIL: grid.at(x=1, y=2) raised {type(e).__name__}: {e}") + + # Test 6: pos= keyword with tuple + try: + point6 = grid.at(pos=(4, 5)) + if point6 is None: + errors.append("Test 6 FAIL: grid.at(pos=(4, 5)) returned None") + else: + print("Test 6 PASS: grid.at(pos=(4, 5)) works") + except Exception as e: + errors.append(f"Test 6 FAIL: grid.at(pos=(4, 5)) raised {type(e).__name__}: {e}") + + # Test 7: pos= keyword with Vector + try: + vec2 = mcrfpy.Vector(6, 7) + point7 = grid.at(pos=vec2) + if point7 is None: + errors.append("Test 7 FAIL: grid.at(pos=Vector(6, 7)) returned None") + else: + print("Test 7 PASS: grid.at(pos=Vector(6, 7)) works") + except Exception as e: + errors.append(f"Test 7 FAIL: grid.at(pos=Vector(6, 7)) raised {type(e).__name__}: {e}") + + # Test 8: pos= keyword with list + try: + point8 = grid.at(pos=[8, 9]) + if point8 is None: + errors.append("Test 8 FAIL: grid.at(pos=[8, 9]) returned None") + else: + print("Test 8 PASS: grid.at(pos=[8, 9]) works") + except Exception as e: + errors.append(f"Test 8 FAIL: grid.at(pos=[8, 9]) raised {type(e).__name__}: {e}") + + # Test 9: Out of range should raise IndexError (not TypeError) + try: + grid.at(100, 100) + errors.append("Test 9 FAIL: grid.at(100, 100) should have raised IndexError") + except IndexError: + print("Test 9 PASS: grid.at(100, 100) raises IndexError") + except Exception as e: + errors.append(f"Test 9 FAIL: grid.at(100, 100) raised {type(e).__name__} instead of IndexError: {e}") + + # Test 10: Invalid type should raise TypeError + try: + grid.at("invalid") + errors.append("Test 10 FAIL: grid.at('invalid') should have raised TypeError") + except TypeError: + print("Test 10 PASS: grid.at('invalid') raises TypeError") + except Exception as e: + errors.append(f"Test 10 FAIL: grid.at('invalid') raised {type(e).__name__} instead of TypeError: {e}") + + # Test 11: Float integers should work (e.g., 3.0 is valid as int) + try: + point11 = grid.at(3.0, 4.0) + if point11 is None: + errors.append("Test 11 FAIL: grid.at(3.0, 4.0) returned None") + else: + print("Test 11 PASS: grid.at(3.0, 4.0) works (float integers)") + except Exception as e: + errors.append(f"Test 11 FAIL: grid.at(3.0, 4.0) raised {type(e).__name__}: {e}") + + # Test 12: Non-integer float should raise TypeError + try: + grid.at(3.5, 4.5) + errors.append("Test 12 FAIL: grid.at(3.5, 4.5) should have raised TypeError") + except TypeError: + print("Test 12 PASS: grid.at(3.5, 4.5) raises TypeError for non-integer floats") + except Exception as e: + errors.append(f"Test 12 FAIL: grid.at(3.5, 4.5) raised {type(e).__name__} instead of TypeError: {e}") + + # Summary + print() + print("=" * 50) + if errors: + print(f"FAILED: {len(errors)} test(s) failed") + for err in errors: + print(f" - {err}") + sys.exit(1) + else: + print("SUCCESS: All 12 tests passed!") + sys.exit(0) + +# Run tests immediately (no game loop needed for this) +test_grid_at_position_parsing()