diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index ab86bab..b6f5eb3 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -124,18 +124,16 @@ void GameEngine::cleanup() McRFPy_API::game = nullptr; } - // 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 + // Shutdown ImGui before closing window 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 16f02c4..4cc7af4 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -4,7 +4,6 @@ #include "PyColor.h" #include "PyTexture.h" #include "PyFOV.h" -#include "PyPositionHelper.h" #include // ============================================================================= @@ -563,18 +562,10 @@ void TileLayer::render(sf::RenderTarget& target, // ============================================================================= PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = { - {"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"}, + {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS, + "at(x, y) -> Color\n\nGet the color at cell position (x, y)."}, {"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS, - "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"}, + "set(x, y, color)\n\nSet the color at cell position (x, y)."}, {"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, @@ -655,9 +646,9 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py return 0; } -PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { +PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } @@ -687,14 +678,9 @@ PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args } PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) { - PyObject* pos_obj; - PyObject* color_obj; - if (!PyArg_ParseTuple(args, "OO", &pos_obj, &color_obj)) { - return NULL; - } - int x, y; - if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + PyObject* color_obj; + if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) { return NULL; } @@ -1122,18 +1108,10 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) { // ============================================================================= PyMethodDef PyGridLayerAPI::TileLayer_methods[] = { - {"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"}, + {"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."}, {"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS, - "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)"}, + "set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -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, @@ -1212,9 +1190,9 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb return 0; } -PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { +PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } @@ -1232,14 +1210,8 @@ PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, } PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) { - 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)) { + int x, y, index; + if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) { return NULL; } diff --git a/src/GridLayers.h b/src/GridLayers.h index 02a5e43..a4656ac 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, PyObject* kwds); + static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args); 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, PyObject* kwds); + static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args); 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 7ca8fe2..a089981 100644 --- a/src/McRFPy_Automation.cpp +++ b/src/McRFPy_Automation.cpp @@ -1,7 +1,6 @@ #include "McRFPy_Automation.h" #include "McRFPy_API.h" #include "GameEngine.h" -#include "PyPositionHelper.h" #include #include #include @@ -38,20 +37,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}, @@ -64,13 +63,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}, @@ -80,14 +79,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}, @@ -96,14 +95,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; @@ -154,22 +153,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); } @@ -177,11 +176,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); } @@ -236,77 +235,51 @@ PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) { return NULL; } -// Get current mouse position - returns Vector object +// Get current mouse position PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) { - int x, y; - auto engine = getGameEngine(); if (!engine || !engine->getRenderTargetPtr()) { - x = simulated_mouse_pos.x; - y = simulated_mouse_pos.y; + return Py_BuildValue("(ii)", simulated_mouse_pos.x, 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; + + // In headless mode, return the simulated mouse position (#111) + if (engine->isHeadless()) { + return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y); } - else if (auto* window = dynamic_cast(engine->getRenderTargetPtr())) { - // In windowed mode, return the actual mouse position relative to window + + // 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); - x = pos.x; - y = pos.y; - } - else { - // Fallback to simulated position - x = simulated_mouse_pos.x; - y = simulated_mouse_pos.y; + return Py_BuildValue("(ii)", pos.x, 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; + // Fallback to simulated position + return Py_BuildValue("(ii)", simulated_mouse_pos.x, simulated_mouse_pos.y); } -// Get screen size - returns Vector object +// Get screen size 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; + return Py_BuildValue("(ii)", 1024, 768); // Default size } - + 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; + return Py_BuildValue("(ii)", size.x, size.y); } -// Check if coordinates are on screen - accepts onScreen(x, y) or onScreen(pos) -PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args, PyObject* kwargs) { +// Check if coordinates are on screen +PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwargs, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &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; @@ -315,101 +288,84 @@ PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args, PyObject* } } -// Move mouse to position - accepts moveTo(pos, duration) +// Move mouse to position PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", "duration", NULL}; - PyObject* pos_obj; - float duration = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast(kwlist), - &pos_obj, &duration)) { - return NULL; - } - + static const char* kwlist[] = {"x", "y", "duration", NULL}; int x, y; - if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) { + float duration = 0.0f; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast(kwlist), + &x, &y, &duration)) { 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 - accepts moveRel(offset, duration) +// Move mouse relative PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"offset", "duration", NULL}; - PyObject* offset_obj; - float duration = 0.0f; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|f", const_cast(kwlist), - &offset_obj, &duration)) { - return NULL; - } - + static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL}; int xOffset, yOffset; - if (!PyPosition_FromObjectInt(offset_obj, &xOffset, &yOffset)) { + float duration = 0.0f; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast(kwlist), + &xOffset, &yOffset, &duration)) { return NULL; } - - // Get current position from Vector + + // Get current position PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - // Extract position from Vector object + int currentX, currentY; - if (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) { + if (!PyArg_ParseTuple(pos, "ii", ¤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 - accepts click(pos, clicks, interval, button) or click() for current position +// Click implementation PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", "clicks", "interval", "button", NULL}; - PyObject* pos_obj = Py_None; + static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL}; + int x = -1, y = -1; int clicks = 1; float interval = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Oifs", const_cast(kwlist), - &pos_obj, &clicks, &interval, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast(kwlist), + &x, &y, &clicks, &interval, &button)) { return NULL; } - - int x, y; - - // If no position specified or None, use current position - if (pos_obj == Py_None) { + + // If no position specified, use current position + if (x == -1 || y == -1) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyPosition_FromObjectInt(pos, &x, &y)) { + + if (!PyArg_ParseTuple(pos, "ii", &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) { @@ -417,61 +373,59 @@ 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 - accepts rightClick(pos) or rightClick() for current position +// Right click PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", NULL}; - PyObject* pos_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { + static const char* kwlist[] = {"x", "y", NULL}; + int x = -1, y = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { return NULL; } - + // Build new args with button="right" PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right")); - if (pos_obj != Py_None) { - PyDict_SetItemString(newKwargs, "pos", pos_obj); - } - + if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); + if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; } -// Double click - accepts doubleClick(pos) or doubleClick() for current position +// Double click PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", NULL}; - PyObject* pos_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { + static const char* kwlist[] = {"x", "y", NULL}; + int x = -1, y = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2)); PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1)); - if (pos_obj != Py_None) { - PyDict_SetItemString(newKwargs, "pos", pos_obj); - } - + if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); + if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; @@ -482,20 +436,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); @@ -508,7 +462,7 @@ PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject injectTextEvent(static_cast(c)); } } - + Py_RETURN_NONE; } @@ -518,13 +472,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; } @@ -535,13 +489,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; } @@ -554,7 +508,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); @@ -562,226 +516,198 @@ 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 - accepts scroll(clicks, pos) or scroll(clicks) for current position +// Scroll wheel PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"clicks", "pos", NULL}; + static const char* kwlist[] = {"clicks", "x", "y", NULL}; int clicks; - PyObject* pos_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|O", const_cast(kwlist), - &clicks, &pos_obj)) { + int x = -1, y = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast(kwlist), + &clicks, &x, &y)) { return NULL; } - - int x, y; - - // If no position specified or None, use current position - if (pos_obj == Py_None) { + + // If no position specified, use current position + if (x == -1 || y == -1) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyPosition_FromObjectInt(pos, &x, &y)) { + + if (!PyArg_ParseTuple(pos, "ii", &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 - accepts middleClick(pos) or middleClick() +// Other click types using the main click function PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", NULL}; - PyObject* pos_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { + static const char* kwlist[] = {"x", "y", NULL}; + int x = -1, y = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle")); - if (pos_obj != Py_None) { - PyDict_SetItemString(newKwargs, "pos", pos_obj); - } - + if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); + if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); + 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[] = {"pos", NULL}; - PyObject* pos_obj = Py_None; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O", const_cast(kwlist), &pos_obj)) { + static const char* kwlist[] = {"x", "y", NULL}; + int x = -1, y = -1; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast(kwlist), &x, &y)) { return NULL; } - + PyObject* newKwargs = PyDict_New(); PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3)); PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1)); - if (pos_obj != Py_None) { - PyDict_SetItemString(newKwargs, "pos", pos_obj); - } - + if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x)); + if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y)); + PyObject* result = _click(self, PyTuple_New(0), newKwargs); Py_DECREF(newKwargs); return result; } -// Mouse button press/release - accepts mouseDown(pos, button) or mouseDown() for current position +// Mouse button press/release PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", "button", NULL}; - PyObject* pos_obj = Py_None; + static const char* kwlist[] = {"x", "y", "button", NULL}; + int x = -1, y = -1; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast(kwlist), - &pos_obj, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast(kwlist), + &x, &y, &button)) { return NULL; } - - int x, y; - - // If no position specified or None, use current position - if (pos_obj == Py_None) { + + // If no position specified, use current position + if (x == -1 || y == -1) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyPosition_FromObjectInt(pos, &x, &y)) { + + if (!PyArg_ParseTuple(pos, "ii", &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[] = {"pos", "button", NULL}; - PyObject* pos_obj = Py_None; + static const char* kwlist[] = {"x", "y", "button", NULL}; + int x = -1, y = -1; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|Os", const_cast(kwlist), - &pos_obj, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast(kwlist), + &x, &y, &button)) { return NULL; } - - int x, y; - - // If no position specified or None, use current position - if (pos_obj == Py_None) { + + // If no position specified, use current position + if (x == -1 || y == -1) { PyObject* pos = _position(self, NULL); if (!pos) return NULL; - - if (!PyPosition_FromObjectInt(pos, &x, &y)) { + + if (!PyArg_ParseTuple(pos, "ii", &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 - accepts dragTo(pos, duration, button) +// Drag operations PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) { - static const char* kwlist[] = {"pos", "duration", "button", NULL}; - PyObject* pos_obj; + static const char* kwlist[] = {"x", "y", "duration", "button", NULL}; + int x, y; float duration = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast(kwlist), - &pos_obj, &duration, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast(kwlist), + &x, &y, &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 (!PyPosition_FromObjectInt(pos, &startX, &startY)) { + if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); - - // Mouse down at current position - create position tuple for the call - PyObject* startPosObj = Py_BuildValue("(ii)", startX, startY); + + // Mouse down at current position + PyObject* downArgs = 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, PyTuple_New(0), downKwargs); - Py_DECREF(startPosObj); + + PyObject* downResult = _mouseDown(self, downArgs, downKwargs); + Py_DECREF(downArgs); Py_DECREF(downKwargs); if (!downResult) return NULL; Py_DECREF(downResult); - + // Move to target position if (duration > 0) { // Smooth movement @@ -795,111 +721,103 @@ PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* k } else { injectMouseEvent(sf::Event::MouseMoved, x, y); } - + // Mouse up at target position - PyObject* endPosObj = Py_BuildValue("(ii)", x, y); + PyObject* upArgs = 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, PyTuple_New(0), upKwargs); - Py_DECREF(endPosObj); + + PyObject* upResult = _mouseUp(self, upArgs, upKwargs); + Py_DECREF(upArgs); 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[] = {"offset", "duration", "button", NULL}; - PyObject* offset_obj; + static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL}; + int xOffset, yOffset; float duration = 0.0f; const char* button = "left"; - - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|fs", const_cast(kwlist), - &offset_obj, &duration, &button)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast(kwlist), + &xOffset, &yOffset, &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 (!PyPosition_FromObjectInt(pos, ¤tX, ¤tY)) { + if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) { Py_DECREF(pos); return NULL; } Py_DECREF(pos); - + // Call dragTo with absolute position - PyObject* targetPos = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset); + PyObject* dragArgs = 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, PyTuple_New(0), dragKwargs); - Py_DECREF(targetPos); + + PyObject* result = _dragTo(self, dragArgs, dragKwargs); + Py_DECREF(dragArgs); 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 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, + + {"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, "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} }; @@ -918,6 +836,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 a090fc3..02a6799 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, PyObject* kwargs); + static PyObject* _onScreen(PyObject* self, PyObject* args); // Mouse movement static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs); diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index 1c6b1ae..6fed830 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -1,7 +1,6 @@ #include "PyCallable.h" #include "McRFPy_API.h" #include "GameEngine.h" -#include "PyVector.h" PyCallable::PyCallable(PyObject* _target) { @@ -50,24 +49,7 @@ PyClickCallable::PyClickCallable() void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action) { - // 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* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), action.c_str()); PyObject* retval = PyCallable::call(args, NULL); if (!retval) { diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp index 9ccadd6..1931626 100644 --- a/src/PyDrawable.cpp +++ b/src/PyDrawable.cpp @@ -1,14 +1,13 @@ #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; @@ -36,20 +35,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; } @@ -66,7 +65,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; } @@ -89,11 +88,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; } @@ -103,7 +102,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 (pos: Vector, button: str, action: str)." + "Function receives (x, y) coordinates of click." ), NULL}, {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, MCRF_PROPERTY(z_index, @@ -131,25 +130,25 @@ static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUS } // move method implementation (#98) -static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args, PyObject* kwds) +static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args) { float dx, dy; - if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) { + if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { return NULL; } - + self->data->move(dx, dy); Py_RETURN_NONE; } // resize method implementation (#98) -static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args, PyObject* kwds) +static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) { float w, h; - if (!PyPosition_ParseFloat(args, kwds, &w, &h)) { + if (!PyArg_ParseTuple(args, "ff", &w, &h)) { return NULL; } - + self->data->resize(w, h); Py_RETURN_NONE; } @@ -163,27 +162,23 @@ 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 | METH_KEYWORDS, + {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, MCRF_METHOD(Drawable, move, - MCRF_SIG("(dx, dy) or (delta)", "None"), + MCRF_SIG("(dx: float, dy: float)", "None"), MCRF_DESC("Move the element by a relative offset."), MCRF_ARGS_START - 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)).") + 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.") )}, - {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS | METH_KEYWORDS, + {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, MCRF_METHOD(Drawable, resize, - MCRF_SIG("(width, height) or (size)", "None"), + MCRF_SIG("(width: float, height: float)", "None"), MCRF_DESC("Resize the element to new dimensions."), MCRF_ARGS_START - 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)).") + 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.") )}, {NULL} // Sentinel }; @@ -213,4 +208,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 92bc9ee..1f46820 100644 --- a/src/PyPositionHelper.h +++ b/src/PyPositionHelper.h @@ -3,32 +3,7 @@ #include "PyVector.h" #include "McRFPy_API.h" -// ============================================================================ -// 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) { ... } -// ============================================================================ - +// Helper class for standardized position argument parsing across UI classes class PyPositionHelper { public: // Template structure for parsing results @@ -37,315 +12,33 @@ 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 (PyTuple_Size(args) > start_index + 1) { + if (!kwds && 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 (extract_number(first, &x) && extract_number(second, &y)) { + 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); result.x = x; result.y = y; result.has_position = true; @@ -353,100 +46,119 @@ public: return result; } } - - // Check for single positional argument that might be tuple, list, or Vector - if (PyTuple_Size(args) > start_index) { + + // Check for single positional argument that might be tuple or Vector + if (!kwds && PyTuple_Size(args) > start_index) { PyObject* first = PyTuple_GetItem(args, start_index); - if (extract_from_iterable(first, &x, &y)) { - result.x = x; - result.y = y; + PyVectorObject* vec = PyVector::from_arg(first); + if (vec) { + result.x = vec->data.x; + result.y = vec->data.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 (extract_number(x_obj, &x) && extract_number(y_obj, &y)) { - result.x = x; - result.y = y; + 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); result.has_position = true; return result; } } - + if (pos_kw) { - if (extract_from_iterable(pos_kw, &x, &y)) { - result.x = x; - result.y = y; + PyVectorObject* vec = PyVector::from_arg(pos_kw); + if (vec) { + result.x = vec->data.x; + result.y = vec->data.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; - 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 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; + } } - + + // 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), Vector, or 2-element sequence"); + "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector"); } - + static void set_position_int_error() { PyErr_SetString(PyExc_TypeError, - "Position must be integers specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence"); + "Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values"); } -}; - -// ============================================================================ -// 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 +}; \ No newline at end of file diff --git a/src/UIArc.cpp b/src/UIArc.cpp index a65054d..e2e93ae 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. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UIARC}, + "Callable executed when arc is clicked.", (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 36f1ab7..570571b 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,7 +1,6 @@ #pragma once #include "Python.h" #include "McRFPy_Doc.h" -#include "PyPositionHelper.h" #include class UIEntity; @@ -53,23 +52,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, PyObject* kwds) +static PyObject* UIDrawable_move(T* self, PyObject* args) { float dx, dy; - if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) { + if (!PyArg_ParseTuple(args, "ff", &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, PyObject* kwds) +static PyObject* UIDrawable_resize(T* self, PyObject* args) { float w, h; - if (!PyPosition_ParseFloat(args, kwds, &w, &h)) { + if (!PyArg_ParseTuple(args, "ff", &w, &h)) { return NULL; } @@ -98,25 +97,23 @@ 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 | METH_KEYWORDS, \ + {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ MCRF_METHOD(Drawable, move, \ - MCRF_SIG("(dx, dy) or (delta)", "None"), \ + MCRF_SIG("(dx: float, dy: float)", "None"), \ MCRF_DESC("Move the element by a relative offset."), \ MCRF_ARGS_START \ - 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)).") \ + 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.") \ )}, \ - {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS | METH_KEYWORDS, \ + {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ MCRF_METHOD(Drawable, resize, \ - MCRF_SIG("(width, height) or (size)", "None"), \ + MCRF_SIG("(width: float, height: float)", "None"), \ MCRF_DESC("Resize the element to new dimensions."), \ MCRF_ARGS_START \ - 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)).") \ + 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.") \ )} // Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives) @@ -225,12 +222,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 (pos: Vector, button: str, action: str) when mouse enters this element's bounds." \ + "Called with (x, y, button, action) 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 (pos: Vector, button: str, action: str) when mouse leaves this element's bounds." \ + "Called with (x, y, button, action) when mouse leaves this element's bounds." \ ), (void*)type_enum}, \ {"hovered", (getter)UIDrawable::get_hovered, NULL, \ MCRF_PROPERTY(hovered, \ @@ -240,7 +237,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 (pos: Vector, button: str, action: str) for each mouse movement while inside. " \ + "Called with (x, y, button, action) 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 d5fb758..155342a 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 (pos: Vector, button: str, action: str)." + "Function receives (x, y) coordinates of click." ), (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 04322c0..7cfa5da 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. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UICIRCLE}, + "Callable executed when circle is clicked.", (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 fe13678..da5b8cb 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -10,7 +10,6 @@ #include "Animation.h" #include "PyAnimation.h" #include "PyEasing.h" -#include "PyPositionHelper.h" // UIDrawable methods now in UIBase.h #include "UIEntityPyMethods.h" @@ -95,17 +94,18 @@ void UIEntity::updateVisibility() } } -PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { +PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { - return NULL; // Error already set by PyPosition_ParseInt + if (!PyArg_ParseTuple(o, "ii", &x, &y)) { + PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)"); + return NULL; } - + 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* args, PyObject* kwds) { 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,14 +590,21 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - 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 + 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; + } } - + // 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"); @@ -736,32 +743,19 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO } PyMethodDef UIEntity::methods[] = { - {"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))"}, + {"at", (PyCFunction)UIEntity::at, METH_O}, {"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, y) or path_to(target) -> list\n\n" - "Find a path to the target position using Dijkstra pathfinding.\n\n" + "path_to(x: int, y: int) -> bool\n\n" + "Find and follow path to target position using A* pathfinding.\n\n" "Args:\n" - " x, y: Target coordinates as two integers, OR\n" - " target: Target coordinates as tuple, list, or Vector\n\n" + " x: Target X coordinate\n" + " y: Target Y coordinate\n\n" "Returns:\n" - " 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))"}, + " 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."}, {"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS, "update_visibility() -> None\n\n" "Update entity's visibility state based on current FOV.\n\n" @@ -805,32 +799,19 @@ 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_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))"}, + {"at", (PyCFunction)UIEntity::at, METH_O}, {"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, y) or path_to(target) -> list\n\n" - "Find a path to the target position using Dijkstra pathfinding.\n\n" + "path_to(x: int, y: int) -> bool\n\n" + "Find and follow path to target position using A* pathfinding.\n\n" "Args:\n" - " x, y: Target coordinates as two integers, OR\n" - " target: Target coordinates as tuple, list, or Vector\n\n" + " x: Target X coordinate\n" + " y: Target Y coordinate\n\n" "Returns:\n" - " 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))"}, + " 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."}, {"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 757659c..f3313d9 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* args, PyObject* kwds); + static PyObject* at(PyUIEntityObject* self, PyObject* o); 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 16dfada..7103212 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 (pos: Vector, button: str, action: str)." + "Function receives (x, y) coordinates of click." ), (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 02c4b2a..dfdf809 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -5,7 +5,6 @@ #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 @@ -686,24 +685,15 @@ 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) { - // 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); - } - } + 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); } } } @@ -719,24 +709,15 @@ 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) { - // 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); - } - } + 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); } // Don't return this - no click_callable to call } @@ -1160,14 +1141,36 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) { PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - 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 + 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; + } } - + // 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); @@ -1346,22 +1349,16 @@ 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[] = {"pos", "radius", "light_walls", "algorithm", NULL}; - PyObject* pos_obj = NULL; - int radius = 0; + static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL}; + int x, y, radius = 0; int light_walls = 1; int algorithm = FOV_BASIC; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast(kwlist), - &pos_obj, &radius, &light_walls, &algorithm)) { + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast(kwlist), + &x, &y, &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); @@ -1370,42 +1367,33 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* Py_RETURN_NONE; } -PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds) +PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &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[] = {"start", "end", "diagonal_cost", NULL}; - PyObject* start_obj = NULL; - PyObject* end_obj = NULL; - float diagonal_cost = 1.41f; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast(kwlist), - &start_obj, &end_obj, &diagonal_cost)) { - return NULL; - } - + static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL}; int x1, y1, x2, y2; - if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) { + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast(kwlist), + &x1, &y1, &x2, &y2, &diagonal_cost)) { 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) { @@ -1414,93 +1402,80 @@ 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", "diagonal_cost", NULL}; - PyObject* root_obj = NULL; - float diagonal_cost = 1.41f; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast(kwlist), - &root_obj, &diagonal_cost)) { - return NULL; - } - + static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL}; int root_x, root_y; - if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) { + float diagonal_cost = 1.41f; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast(kwlist), + &root_x, &root_y, &diagonal_cost)) { 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* kwds) +PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &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* kwds) +PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args) { int x, y; - if (!PyPosition_ParseInt(args, kwds, &x, &y)) { + if (!PyArg_ParseTuple(args, "ii", &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) { - 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, "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)) { + 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)) { 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; } @@ -1837,63 +1812,72 @@ 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(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "compute_fov(x: int, y: int, 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" - " pos: Position as (x, y) tuple, list, or Vector\n" + " x: X coordinate of the viewer\n" + " y: Y coordinate of the viewer\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(pos) to query visibility."}, - {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS, - "is_in_fov(pos) -> bool\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" "Check if a cell is in the field of view.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: X coordinate to check\n" + " y: Y coordinate to check\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(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + {"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 A* path between two points.\n\n" "Args:\n" - " start: Starting position as (x, y) tuple, list, or Vector\n" - " end: Target position as (x, y) tuple, list, or Vector\n" + " x1: Starting X coordinate\n" + " y1: Starting Y coordinate\n" + " x2: Target X coordinate\n" + " y2: Target Y coordinate\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, diagonal_cost: float = 1.41) -> None\n\n" + {"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 map from root position.\n\n" "Args:\n" - " root: Root position as (x, y) tuple, list, or Vector\n" + " root_x: X coordinate of the root/target\n" + " root_y: Y coordinate of the root/target\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 | METH_KEYWORDS, - "get_dijkstra_distance(pos) -> Optional[float]\n\n" + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, + "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" "Get distance from Dijkstra root to position.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: X coordinate to query\n" + " y: Y coordinate to query\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 | METH_KEYWORDS, - "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n" + {"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 path from position to Dijkstra root.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: Starting X coordinate\n" + " y: Starting Y coordinate\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(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Compute A* path between two points.\n\n" "Args:\n" - " start: Starting position as (x, y) tuple, list, or Vector\n" - " end: Target position as (x, y) tuple, list, or Vector\n" + " x1: Starting X coordinate\n" + " y1: Starting Y coordinate\n" + " x2: Target X coordinate\n" + " y2: Target Y coordinate\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" @@ -1933,63 +1917,72 @@ 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(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "compute_fov(x: int, y: int, 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" - " pos: Position as (x, y) tuple, list, or Vector\n" + " x: X coordinate of the viewer\n" + " y: Y coordinate of the viewer\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(pos) to query visibility."}, - {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS, - "is_in_fov(pos) -> bool\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" "Check if a cell is in the field of view.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: X coordinate to check\n" + " y: Y coordinate to check\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(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + {"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 A* path between two points.\n\n" "Args:\n" - " start: Starting position as (x, y) tuple, list, or Vector\n" - " end: Target position as (x, y) tuple, list, or Vector\n" + " x1: Starting X coordinate\n" + " y1: Starting Y coordinate\n" + " x2: Target X coordinate\n" + " y2: Target Y coordinate\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, diagonal_cost: float = 1.41) -> None\n\n" + {"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 map from root position.\n\n" "Args:\n" - " root: Root position as (x, y) tuple, list, or Vector\n" + " root_x: X coordinate of the root/target\n" + " root_y: Y coordinate of the root/target\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 | METH_KEYWORDS, - "get_dijkstra_distance(pos) -> Optional[float]\n\n" + {"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS, + "get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n" "Get distance from Dijkstra root to position.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: X coordinate to query\n" + " y: Y coordinate to query\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 | METH_KEYWORDS, - "get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n" + {"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 path from position to Dijkstra root.\n\n" "Args:\n" - " pos: Position as (x, y) tuple, list, or Vector\n\n" + " x: Starting X coordinate\n" + " y: Starting Y coordinate\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(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" + "compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n" "Compute A* path between two points.\n\n" "Args:\n" - " start: Starting position as (x, y) tuple, list, or Vector\n" - " end: Target position as (x, y) tuple, list, or Vector\n" + " x1: Starting X coordinate\n" + " y1: Starting Y coordinate\n" + " x2: Target X coordinate\n" + " y2: Target Y coordinate\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" @@ -2062,7 +2055,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 (pos: Vector, button: str, action: str)." + "Function receives (x, y) coordinates of click." ), (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 @@ -2090,11 +2083,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_pos: Vector).", NULL}, + "Callback when mouse enters a grid cell. Called with (cell_x, cell_y).", 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_pos: Vector).", NULL}, + "Callback when mouse exits a grid cell. Called with (cell_x, cell_y).", 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_pos: Vector).", NULL}, + "Callback when a grid cell is clicked. Called with (cell_x, cell_y).", NULL}, {"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL, "Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL}, {NULL} /* Sentinel */ @@ -2256,47 +2249,29 @@ 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) { - // 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); - } - } + 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); } } // Fire enter callback for new cell if (new_cell.has_value() && on_cell_enter_callable) { - // 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); - } - } + 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); } } diff --git a/src/UIGrid.h b/src/UIGrid.h index ef29e6d..62bb1b0 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, PyObject* kwds); + static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args); 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, PyObject* kwds); - static PyObject* py_get_dijkstra_path(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_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 6312c16..a08429c 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. Function receives (pos: Vector, button: str, action: str)."), + MCRF_PROPERTY(on_click, "Callable executed when line is clicked."), (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 a77fea3..638453c 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 (pos: Vector, button: str, action: str)." + "Function receives (x, y) coordinates of click." ), (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 deleted file mode 100644 index f81aa6b..0000000 --- a/tests/test_callback_vector.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/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 deleted file mode 100644 index 9791dfd..0000000 --- a/tests/unit/automation_vector_test.py +++ /dev/null @@ -1,152 +0,0 @@ -"""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 deleted file mode 100644 index ae2dffd..0000000 --- a/tests/unit/test_drawable_move_resize_position_parsing.py +++ /dev/null @@ -1,129 +0,0 @@ -"""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 deleted file mode 100644 index 716c178..0000000 --- a/tests/unit/test_entity_position_parsing.py +++ /dev/null @@ -1,135 +0,0 @@ -"""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 deleted file mode 100644 index ec942b6..0000000 --- a/tests/unit/test_grid_pathfinding_positions.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/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 deleted file mode 100644 index f097a5e..0000000 --- a/tests/unit/test_layer_position_parsing.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/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 deleted file mode 100644 index ab4b335..0000000 --- a/tests/unit/test_position_helper.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/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()