Compare commits

...

3 commits

Author SHA1 Message Date
9eacedc624 Input Enums instead of strings. 2026-01-10 21:31:20 -05:00
d9411f94a4 Version bump: 0.2.0-prerelease-7drl2026 (d6ef29f) -> 0.2.1-prerelease-7drl2026 2026-01-10 08:55:50 -05:00
d6ef29f3cd Grid code quality improvements
* Grid [x, y] subscript - convenience for `.at()`
* Extract UIEntityCollection - cleanup of UIGrid.cpp
* Thread-safe type cache - PyTypeCache
* Exception-safe extend() - validate before modify
2026-01-10 08:37:31 -05:00
16 changed files with 2698 additions and 1098 deletions

View file

@ -3,6 +3,7 @@
#include "McRFPy_Automation.h"
#include "McRFPy_Libtcod.h"
#include "McRFPy_Doc.h"
#include "PyTypeCache.h" // Thread-safe cached Python types
#include "platform.h"
#include "PyAnimation.h"
#include "PyDrawable.h"
@ -12,6 +13,9 @@
#include "PyFOV.h"
#include "PyTransition.h"
#include "PyEasing.h"
#include "PyKey.h"
#include "PyMouseButton.h"
#include "PyInputState.h"
#include "PySound.h"
#include "PyMusic.h"
#include "PyKeyboard.h"
@ -535,6 +539,27 @@ PyObject* PyInit_mcrfpy()
PyErr_Clear();
}
// Add Key enum class for keyboard input
PyObject* key_class = PyKey::create_enum_class(m);
if (!key_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Add MouseButton enum class for mouse input
PyObject* mouse_button_class = PyMouseButton::create_enum_class(m);
if (!mouse_button_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Add InputState enum class for input event states (pressed/released)
PyObject* input_state_class = PyInputState::create_enum_class(m);
if (!input_state_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) {
@ -549,12 +574,20 @@ PyObject* PyInit_mcrfpy()
PyObject* libtcod_module = McRFPy_Libtcod::init_libtcod_module();
if (libtcod_module != NULL) {
PyModule_AddObject(m, "libtcod", libtcod_module);
// Also add to sys.modules for proper import behavior
PyObject* sys_modules = PyImport_GetModuleDict();
PyDict_SetItemString(sys_modules, "mcrfpy.libtcod", libtcod_module);
}
// Initialize PyTypeCache for thread-safe type lookups
// This must be done after all types are added to the module
if (!PyTypeCache::initialize(m)) {
// Failed to initialize type cache - this is a critical error
// Error message already set by PyTypeCache::initialize
return NULL;
}
//McRFPy_API::mcrf_module = m;
return m;
}

View file

@ -1,4 +1,4 @@
#pragma once
// McRogueFace version string (#164)
#define MCRFPY_VERSION "0.2.0-prerelease-7drl2026"
#define MCRFPY_VERSION "0.2.1-prerelease-7drl2026"

187
src/PyInputState.cpp Normal file
View file

@ -0,0 +1,187 @@
#include "PyInputState.h"
#include <sstream>
// Static storage for cached enum class reference
PyObject* PyInputState::input_state_enum_class = nullptr;
// InputState entries - maps enum name to value and legacy string
struct InputStateEntry {
const char* name; // Python enum name (UPPER_SNAKE_CASE)
int value; // Integer value
const char* legacy; // Legacy string name for backwards compatibility
};
static const InputStateEntry input_state_table[] = {
{"PRESSED", 0, "start"},
{"RELEASED", 1, "end"},
};
static const int NUM_INPUT_STATE_ENTRIES = sizeof(input_state_table) / sizeof(input_state_table[0]);
const char* PyInputState::to_legacy_string(bool pressed) {
return pressed ? "start" : "end";
}
PyObject* PyInputState::create_enum_class(PyObject* module) {
// Build the enum definition dynamically from the table
std::ostringstream code;
code << "from enum import IntEnum\n\n";
code << "class InputState(IntEnum):\n";
code << " \"\"\"Enum representing input event states (pressed/released).\n";
code << " \n";
code << " Values:\n";
code << " PRESSED: Key or button was pressed (legacy: 'start')\n";
code << " RELEASED: Key or button was released (legacy: 'end')\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " InputState.PRESSED == 'start' # True\n";
code << " InputState.RELEASED == 'end' # True\n";
code << " \"\"\"\n";
// Add enum members
for (int i = 0; i < NUM_INPUT_STATE_ENTRIES; i++) {
code << " " << input_state_table[i].name << " = " << input_state_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "InputState._legacy_names = {\n";
for (int i = 0; i < NUM_INPUT_STATE_ENTRIES; i++) {
code << " " << input_state_table[i].value << ": \"" << input_state_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _InputState_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "PRESSED")
if self.name == other:
return True
# Check legacy name match (e.g., "start")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
InputState.__eq__ = _InputState_eq
InputState.__hash__ = lambda self: hash(int(self))
InputState.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
InputState.__str__ = lambda self: self.name
)";
std::string code_str = code.str();
// Create globals with builtins
PyObject* globals = PyDict_New();
if (!globals) return NULL;
PyObject* builtins = PyEval_GetBuiltins();
PyDict_SetItemString(globals, "__builtins__", builtins);
PyObject* locals = PyDict_New();
if (!locals) {
Py_DECREF(globals);
return NULL;
}
// Execute the code to create the enum
PyObject* result = PyRun_String(code_str.c_str(), Py_file_input, globals, locals);
if (!result) {
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_DECREF(result);
// Get the InputState class from locals
PyObject* input_state_class = PyDict_GetItemString(locals, "InputState");
if (!input_state_class) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create InputState enum class");
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_INCREF(input_state_class);
// Cache the reference for fast type checking
input_state_enum_class = input_state_class;
Py_INCREF(input_state_enum_class);
// Add to module
if (PyModule_AddObject(module, "InputState", input_state_class) < 0) {
Py_DECREF(input_state_class);
Py_DECREF(globals);
Py_DECREF(locals);
input_state_enum_class = nullptr;
return NULL;
}
Py_DECREF(globals);
Py_DECREF(locals);
return input_state_class;
}
int PyInputState::from_arg(PyObject* arg, bool* out_pressed) {
// Accept InputState enum member
if (input_state_enum_class && PyObject_IsInstance(arg, input_state_enum_class)) {
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
*out_pressed = (val == 0); // PRESSED = 0
return 1;
}
// Accept int
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val == 0 || val == 1) {
*out_pressed = (val == 0);
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid InputState value: %ld. Must be 0 (PRESSED) or 1 (RELEASED).", val);
return 0;
}
// Accept string (both new and legacy names)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_INPUT_STATE_ENTRIES; i++) {
if (strcmp(name, input_state_table[i].name) == 0 ||
strcmp(name, input_state_table[i].legacy) == 0) {
*out_pressed = (input_state_table[i].value == 0);
return 1;
}
}
PyErr_Format(PyExc_ValueError,
"Unknown InputState: '%s'. Use InputState.PRESSED, InputState.RELEASED, "
"or legacy strings 'start', 'end'.", name);
return 0;
}
PyErr_SetString(PyExc_TypeError,
"InputState must be mcrfpy.InputState enum member, string, or int");
return 0;
}

34
src/PyInputState.h Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include "Common.h"
#include "Python.h"
// Module-level InputState enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.InputState
//
// Values:
// PRESSED = 0 (corresponds to "start" in legacy API)
// RELEASED = 1 (corresponds to "end" in legacy API)
//
// The enum compares equal to both its name ("PRESSED") and legacy string ("start")
class PyInputState {
public:
// Create the InputState enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract input state from Python arg
// Accepts InputState enum, string (for backwards compatibility), int, or None
// Returns 1 on success, 0 on error (with exception set)
// out_pressed is set to true for PRESSED/start, false for RELEASED/end
static int from_arg(PyObject* arg, bool* out_pressed);
// Convert bool to legacy string name (for passing to callbacks)
static const char* to_legacy_string(bool pressed);
// Cached reference to the InputState enum class for fast type checking
static PyObject* input_state_enum_class;
// Number of input states
static const int NUM_INPUT_STATES = 2;
};

335
src/PyKey.cpp Normal file
View file

@ -0,0 +1,335 @@
#include "PyKey.h"
#include <sstream>
#include <cstring>
// Static storage for cached enum class reference
PyObject* PyKey::key_enum_class = nullptr;
// Key entries - maps enum name to SFML value and legacy string
struct KeyEntry {
const char* name; // Python enum name (UPPER_SNAKE_CASE)
int value; // Integer value (matches sf::Keyboard::Key)
const char* legacy; // Legacy string name for backwards compatibility
};
// Complete key table matching SFML's sf::Keyboard::Key enum
static const KeyEntry key_table[] = {
// Letters (sf::Keyboard::A = 0 through sf::Keyboard::Z = 25)
{"A", sf::Keyboard::A, "A"},
{"B", sf::Keyboard::B, "B"},
{"C", sf::Keyboard::C, "C"},
{"D", sf::Keyboard::D, "D"},
{"E", sf::Keyboard::E, "E"},
{"F", sf::Keyboard::F, "F"},
{"G", sf::Keyboard::G, "G"},
{"H", sf::Keyboard::H, "H"},
{"I", sf::Keyboard::I, "I"},
{"J", sf::Keyboard::J, "J"},
{"K", sf::Keyboard::K, "K"},
{"L", sf::Keyboard::L, "L"},
{"M", sf::Keyboard::M, "M"},
{"N", sf::Keyboard::N, "N"},
{"O", sf::Keyboard::O, "O"},
{"P", sf::Keyboard::P, "P"},
{"Q", sf::Keyboard::Q, "Q"},
{"R", sf::Keyboard::R, "R"},
{"S", sf::Keyboard::S, "S"},
{"T", sf::Keyboard::T, "T"},
{"U", sf::Keyboard::U, "U"},
{"V", sf::Keyboard::V, "V"},
{"W", sf::Keyboard::W, "W"},
{"X", sf::Keyboard::X, "X"},
{"Y", sf::Keyboard::Y, "Y"},
{"Z", sf::Keyboard::Z, "Z"},
// Number row (sf::Keyboard::Num0 = 26 through Num9 = 35)
{"NUM_0", sf::Keyboard::Num0, "Num0"},
{"NUM_1", sf::Keyboard::Num1, "Num1"},
{"NUM_2", sf::Keyboard::Num2, "Num2"},
{"NUM_3", sf::Keyboard::Num3, "Num3"},
{"NUM_4", sf::Keyboard::Num4, "Num4"},
{"NUM_5", sf::Keyboard::Num5, "Num5"},
{"NUM_6", sf::Keyboard::Num6, "Num6"},
{"NUM_7", sf::Keyboard::Num7, "Num7"},
{"NUM_8", sf::Keyboard::Num8, "Num8"},
{"NUM_9", sf::Keyboard::Num9, "Num9"},
// Control keys
{"ESCAPE", sf::Keyboard::Escape, "Escape"},
{"LEFT_CONTROL", sf::Keyboard::LControl, "LControl"},
{"LEFT_SHIFT", sf::Keyboard::LShift, "LShift"},
{"LEFT_ALT", sf::Keyboard::LAlt, "LAlt"},
{"LEFT_SYSTEM", sf::Keyboard::LSystem, "LSystem"},
{"RIGHT_CONTROL", sf::Keyboard::RControl, "RControl"},
{"RIGHT_SHIFT", sf::Keyboard::RShift, "RShift"},
{"RIGHT_ALT", sf::Keyboard::RAlt, "RAlt"},
{"RIGHT_SYSTEM", sf::Keyboard::RSystem, "RSystem"},
{"MENU", sf::Keyboard::Menu, "Menu"},
// Punctuation and symbols
{"LEFT_BRACKET", sf::Keyboard::LBracket, "LBracket"},
{"RIGHT_BRACKET", sf::Keyboard::RBracket, "RBracket"},
{"SEMICOLON", sf::Keyboard::Semicolon, "Semicolon"},
{"COMMA", sf::Keyboard::Comma, "Comma"},
{"PERIOD", sf::Keyboard::Period, "Period"},
{"APOSTROPHE", sf::Keyboard::Apostrophe, "Apostrophe"},
{"SLASH", sf::Keyboard::Slash, "Slash"},
{"BACKSLASH", sf::Keyboard::Backslash, "Backslash"},
{"GRAVE", sf::Keyboard::Grave, "Grave"},
{"EQUAL", sf::Keyboard::Equal, "Equal"},
{"HYPHEN", sf::Keyboard::Hyphen, "Hyphen"},
// Whitespace and editing
{"SPACE", sf::Keyboard::Space, "Space"},
{"ENTER", sf::Keyboard::Enter, "Enter"},
{"BACKSPACE", sf::Keyboard::Backspace, "Backspace"},
{"TAB", sf::Keyboard::Tab, "Tab"},
// Navigation
{"PAGE_UP", sf::Keyboard::PageUp, "PageUp"},
{"PAGE_DOWN", sf::Keyboard::PageDown, "PageDown"},
{"END", sf::Keyboard::End, "End"},
{"HOME", sf::Keyboard::Home, "Home"},
{"INSERT", sf::Keyboard::Insert, "Insert"},
{"DELETE", sf::Keyboard::Delete, "Delete"},
// Numpad operators
{"ADD", sf::Keyboard::Add, "Add"},
{"SUBTRACT", sf::Keyboard::Subtract, "Subtract"},
{"MULTIPLY", sf::Keyboard::Multiply, "Multiply"},
{"DIVIDE", sf::Keyboard::Divide, "Divide"},
// Arrow keys
{"LEFT", sf::Keyboard::Left, "Left"},
{"RIGHT", sf::Keyboard::Right, "Right"},
{"UP", sf::Keyboard::Up, "Up"},
{"DOWN", sf::Keyboard::Down, "Down"},
// Numpad numbers (sf::Keyboard::Numpad0 = 75 through Numpad9 = 84)
{"NUMPAD_0", sf::Keyboard::Numpad0, "Numpad0"},
{"NUMPAD_1", sf::Keyboard::Numpad1, "Numpad1"},
{"NUMPAD_2", sf::Keyboard::Numpad2, "Numpad2"},
{"NUMPAD_3", sf::Keyboard::Numpad3, "Numpad3"},
{"NUMPAD_4", sf::Keyboard::Numpad4, "Numpad4"},
{"NUMPAD_5", sf::Keyboard::Numpad5, "Numpad5"},
{"NUMPAD_6", sf::Keyboard::Numpad6, "Numpad6"},
{"NUMPAD_7", sf::Keyboard::Numpad7, "Numpad7"},
{"NUMPAD_8", sf::Keyboard::Numpad8, "Numpad8"},
{"NUMPAD_9", sf::Keyboard::Numpad9, "Numpad9"},
// Function keys (sf::Keyboard::F1 = 85 through F15 = 99)
{"F1", sf::Keyboard::F1, "F1"},
{"F2", sf::Keyboard::F2, "F2"},
{"F3", sf::Keyboard::F3, "F3"},
{"F4", sf::Keyboard::F4, "F4"},
{"F5", sf::Keyboard::F5, "F5"},
{"F6", sf::Keyboard::F6, "F6"},
{"F7", sf::Keyboard::F7, "F7"},
{"F8", sf::Keyboard::F8, "F8"},
{"F9", sf::Keyboard::F9, "F9"},
{"F10", sf::Keyboard::F10, "F10"},
{"F11", sf::Keyboard::F11, "F11"},
{"F12", sf::Keyboard::F12, "F12"},
{"F13", sf::Keyboard::F13, "F13"},
{"F14", sf::Keyboard::F14, "F14"},
{"F15", sf::Keyboard::F15, "F15"},
// Misc
{"PAUSE", sf::Keyboard::Pause, "Pause"},
// Unknown key (for completeness)
{"UNKNOWN", sf::Keyboard::Unknown, "Unknown"},
};
static const int NUM_KEY_ENTRIES = sizeof(key_table) / sizeof(key_table[0]);
const char* PyKey::to_legacy_string(sf::Keyboard::Key key) {
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (key_table[i].value == static_cast<int>(key)) {
return key_table[i].legacy;
}
}
return "Unknown";
}
sf::Keyboard::Key PyKey::from_legacy_string(const char* name) {
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (strcmp(key_table[i].legacy, name) == 0 ||
strcmp(key_table[i].name, name) == 0) {
return static_cast<sf::Keyboard::Key>(key_table[i].value);
}
}
return sf::Keyboard::Unknown;
}
PyObject* PyKey::create_enum_class(PyObject* module) {
// Build the enum definition dynamically from the table
std::ostringstream code;
code << "from enum import IntEnum\n\n";
code << "class Key(IntEnum):\n";
code << " \"\"\"Enum representing keyboard keys.\n";
code << " \n";
code << " Values map to SFML's sf::Keyboard::Key enum.\n";
code << " \n";
code << " Categories:\n";
code << " Letters: A-Z\n";
code << " Numbers: NUM_0 through NUM_9 (top row)\n";
code << " Numpad: NUMPAD_0 through NUMPAD_9\n";
code << " Function: F1 through F15\n";
code << " Modifiers: LEFT_SHIFT, RIGHT_SHIFT, LEFT_CONTROL, etc.\n";
code << " Navigation: LEFT, RIGHT, UP, DOWN, HOME, END, PAGE_UP, PAGE_DOWN\n";
code << " Editing: ENTER, BACKSPACE, DELETE, INSERT, TAB, SPACE\n";
code << " Symbols: COMMA, PERIOD, SLASH, SEMICOLON, etc.\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " Key.ESCAPE == 'Escape' # True\n";
code << " Key.LEFT_SHIFT == 'LShift' # True\n";
code << " \"\"\"\n";
// Add enum members
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
code << " " << key_table[i].name << " = " << key_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "Key._legacy_names = {\n";
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
code << " " << key_table[i].value << ": \"" << key_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _Key_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "ESCAPE")
if self.name == other:
return True
# Check legacy name match (e.g., "Escape")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
Key.__eq__ = _Key_eq
Key.__hash__ = lambda self: hash(int(self))
Key.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
Key.__str__ = lambda self: self.name
)";
std::string code_str = code.str();
// Create globals with builtins
PyObject* globals = PyDict_New();
if (!globals) return NULL;
PyObject* builtins = PyEval_GetBuiltins();
PyDict_SetItemString(globals, "__builtins__", builtins);
PyObject* locals = PyDict_New();
if (!locals) {
Py_DECREF(globals);
return NULL;
}
// Execute the code to create the enum
PyObject* result = PyRun_String(code_str.c_str(), Py_file_input, globals, locals);
if (!result) {
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_DECREF(result);
// Get the Key class from locals
PyObject* key_class = PyDict_GetItemString(locals, "Key");
if (!key_class) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create Key enum class");
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_INCREF(key_class);
// Cache the reference for fast type checking
key_enum_class = key_class;
Py_INCREF(key_enum_class);
// Add to module
if (PyModule_AddObject(module, "Key", key_class) < 0) {
Py_DECREF(key_class);
Py_DECREF(globals);
Py_DECREF(locals);
key_enum_class = nullptr;
return NULL;
}
Py_DECREF(globals);
Py_DECREF(locals);
return key_class;
}
int PyKey::from_arg(PyObject* arg, sf::Keyboard::Key* out_key) {
// Accept Key enum member
if (key_enum_class && PyObject_IsInstance(arg, key_enum_class)) {
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
*out_key = static_cast<sf::Keyboard::Key>(val);
return 1;
}
// Accept int
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= -1 && val < sf::Keyboard::KeyCount) {
*out_key = static_cast<sf::Keyboard::Key>(val);
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid Key value: %ld. Must be -1 (Unknown) to %d.", val, sf::Keyboard::KeyCount - 1);
return 0;
}
// Accept string (both new and legacy names)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_KEY_ENTRIES; i++) {
if (strcmp(name, key_table[i].name) == 0 ||
strcmp(name, key_table[i].legacy) == 0) {
*out_key = static_cast<sf::Keyboard::Key>(key_table[i].value);
return 1;
}
}
PyErr_Format(PyExc_ValueError,
"Unknown Key: '%s'. Use Key enum members (e.g., Key.ESCAPE, Key.A) "
"or legacy strings (e.g., 'Escape', 'A').", name);
return 0;
}
PyErr_SetString(PyExc_TypeError,
"Key must be mcrfpy.Key enum member, string, or int");
return 0;
}

44
src/PyKey.h Normal file
View file

@ -0,0 +1,44 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <SFML/Window/Keyboard.hpp>
// Module-level Key enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.Key
//
// Values map to sf::Keyboard::Key enum values.
// The enum compares equal to both its name ("ESCAPE") and legacy string ("Escape")
//
// Naming convention:
// - Letters: A, B, C, ... Z
// - Numbers: NUM_0, NUM_1, ... NUM_9 (top row)
// - Numpad: NUMPAD_0, NUMPAD_1, ... NUMPAD_9
// - Function keys: F1, F2, ... F15
// - Modifiers: LEFT_SHIFT, RIGHT_SHIFT, LEFT_CONTROL, RIGHT_CONTROL, etc.
// - Navigation: LEFT, RIGHT, UP, DOWN, HOME, END, PAGE_UP, PAGE_DOWN
// - Special: ESCAPE, ENTER, SPACE, TAB, BACKSPACE, DELETE, INSERT, PAUSE
class PyKey {
public:
// Create the Key enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract key from Python arg
// Accepts Key enum, string (for backwards compatibility), int, or None
// Returns 1 on success, 0 on error (with exception set)
static int from_arg(PyObject* arg, sf::Keyboard::Key* out_key);
// Convert sf::Keyboard::Key to legacy string name (for passing to callbacks)
static const char* to_legacy_string(sf::Keyboard::Key key);
// Convert legacy string to sf::Keyboard::Key
// Returns sf::Keyboard::Unknown if not found
static sf::Keyboard::Key from_legacy_string(const char* name);
// Cached reference to the Key enum class for fast type checking
static PyObject* key_enum_class;
// Number of keys (matches sf::Keyboard::KeyCount)
static const int NUM_KEYS = sf::Keyboard::KeyCount;
};

205
src/PyMouseButton.cpp Normal file
View file

@ -0,0 +1,205 @@
#include "PyMouseButton.h"
#include <sstream>
// Static storage for cached enum class reference
PyObject* PyMouseButton::mouse_button_enum_class = nullptr;
// MouseButton entries - maps enum name to value and legacy string
struct MouseButtonEntry {
const char* name; // Python enum name (UPPER_SNAKE_CASE)
int value; // Integer value (matches sf::Mouse::Button)
const char* legacy; // Legacy string name for backwards compatibility
};
static const MouseButtonEntry mouse_button_table[] = {
{"LEFT", sf::Mouse::Left, "left"},
{"RIGHT", sf::Mouse::Right, "right"},
{"MIDDLE", sf::Mouse::Middle, "middle"},
{"X1", sf::Mouse::XButton1, "x1"},
{"X2", sf::Mouse::XButton2, "x2"},
};
static const int NUM_MOUSE_BUTTON_ENTRIES = sizeof(mouse_button_table) / sizeof(mouse_button_table[0]);
const char* PyMouseButton::to_legacy_string(sf::Mouse::Button button) {
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
if (mouse_button_table[i].value == static_cast<int>(button)) {
return mouse_button_table[i].legacy;
}
}
return "left"; // Default fallback
}
PyObject* PyMouseButton::create_enum_class(PyObject* module) {
// Build the enum definition dynamically from the table
std::ostringstream code;
code << "from enum import IntEnum\n\n";
code << "class MouseButton(IntEnum):\n";
code << " \"\"\"Enum representing mouse buttons.\n";
code << " \n";
code << " Values:\n";
code << " LEFT: Left mouse button (legacy: 'left')\n";
code << " RIGHT: Right mouse button (legacy: 'right')\n";
code << " MIDDLE: Middle mouse button / scroll wheel click (legacy: 'middle')\n";
code << " X1: Extra mouse button 1 (legacy: 'x1')\n";
code << " X2: Extra mouse button 2 (legacy: 'x2')\n";
code << " \n";
code << " These enum values compare equal to their legacy string equivalents\n";
code << " for backwards compatibility:\n";
code << " MouseButton.LEFT == 'left' # True\n";
code << " MouseButton.RIGHT == 'right' # True\n";
code << " \"\"\"\n";
// Add enum members
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
code << " " << mouse_button_table[i].name << " = " << mouse_button_table[i].value << "\n";
}
// Add legacy names and custom methods AFTER class creation
// (IntEnum doesn't allow dict attributes during class definition)
code << "\n# Add legacy name mapping after class creation\n";
code << "MouseButton._legacy_names = {\n";
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
code << " " << mouse_button_table[i].value << ": \"" << mouse_button_table[i].legacy << "\",\n";
}
code << "}\n\n";
code << R"(
def _MouseButton_eq(self, other):
if isinstance(other, str):
# Check enum name match (e.g., "LEFT")
if self.name == other:
return True
# Check legacy name match (e.g., "left")
legacy = type(self)._legacy_names.get(self.value)
if legacy and legacy == other:
return True
return False
# Fall back to int comparison for IntEnum
return int.__eq__(int(self), other)
MouseButton.__eq__ = _MouseButton_eq
MouseButton.__hash__ = lambda self: hash(int(self))
MouseButton.__repr__ = lambda self: f"{type(self).__name__}.{self.name}"
MouseButton.__str__ = lambda self: self.name
)";
std::string code_str = code.str();
// Create globals with builtins
PyObject* globals = PyDict_New();
if (!globals) return NULL;
PyObject* builtins = PyEval_GetBuiltins();
PyDict_SetItemString(globals, "__builtins__", builtins);
PyObject* locals = PyDict_New();
if (!locals) {
Py_DECREF(globals);
return NULL;
}
// Execute the code to create the enum
PyObject* result = PyRun_String(code_str.c_str(), Py_file_input, globals, locals);
if (!result) {
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_DECREF(result);
// Get the MouseButton class from locals
PyObject* mouse_button_class = PyDict_GetItemString(locals, "MouseButton");
if (!mouse_button_class) {
PyErr_SetString(PyExc_RuntimeError, "Failed to create MouseButton enum class");
Py_DECREF(globals);
Py_DECREF(locals);
return NULL;
}
Py_INCREF(mouse_button_class);
// Cache the reference for fast type checking
mouse_button_enum_class = mouse_button_class;
Py_INCREF(mouse_button_enum_class);
// Add to module
if (PyModule_AddObject(module, "MouseButton", mouse_button_class) < 0) {
Py_DECREF(mouse_button_class);
Py_DECREF(globals);
Py_DECREF(locals);
mouse_button_enum_class = nullptr;
return NULL;
}
Py_DECREF(globals);
Py_DECREF(locals);
return mouse_button_class;
}
int PyMouseButton::from_arg(PyObject* arg, sf::Mouse::Button* out_button) {
// Accept MouseButton enum member
if (mouse_button_enum_class && PyObject_IsInstance(arg, mouse_button_enum_class)) {
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_MOUSE_BUTTON_ENTRIES) {
*out_button = static_cast<sf::Mouse::Button>(val);
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid MouseButton value: %ld. Must be 0-4.", val);
return 0;
}
// Accept int
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_MOUSE_BUTTON_ENTRIES) {
*out_button = static_cast<sf::Mouse::Button>(val);
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid MouseButton value: %ld. Must be 0 (LEFT), 1 (RIGHT), 2 (MIDDLE), "
"3 (X1), or 4 (X2).", val);
return 0;
}
// Accept string (both new and legacy names)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check all entries for both name and legacy match
for (int i = 0; i < NUM_MOUSE_BUTTON_ENTRIES; i++) {
if (strcmp(name, mouse_button_table[i].name) == 0 ||
strcmp(name, mouse_button_table[i].legacy) == 0) {
*out_button = static_cast<sf::Mouse::Button>(mouse_button_table[i].value);
return 1;
}
}
PyErr_Format(PyExc_ValueError,
"Unknown MouseButton: '%s'. Use MouseButton.LEFT, MouseButton.RIGHT, "
"MouseButton.MIDDLE, MouseButton.X1, MouseButton.X2, "
"or legacy strings 'left', 'right', 'middle', 'x1', 'x2'.", name);
return 0;
}
PyErr_SetString(PyExc_TypeError,
"MouseButton must be mcrfpy.MouseButton enum member, string, or int");
return 0;
}

37
src/PyMouseButton.h Normal file
View file

@ -0,0 +1,37 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include <SFML/Window/Mouse.hpp>
// Module-level MouseButton enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.MouseButton
//
// Values map to sf::Mouse::Button:
// LEFT = 0 (corresponds to "left" in legacy API)
// RIGHT = 1 (corresponds to "right" in legacy API)
// MIDDLE = 2 (corresponds to "middle" in legacy API)
// X1 = 3 (extra button 1)
// X2 = 4 (extra button 2)
//
// The enum compares equal to both its name ("LEFT") and legacy string ("left")
class PyMouseButton {
public:
// Create the MouseButton enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract mouse button from Python arg
// Accepts MouseButton enum, string (for backwards compatibility), int, or None
// Returns 1 on success, 0 on error (with exception set)
static int from_arg(PyObject* arg, sf::Mouse::Button* out_button);
// Convert sf::Mouse::Button to legacy string name (for passing to callbacks)
static const char* to_legacy_string(sf::Mouse::Button button);
// Cached reference to the MouseButton enum class for fast type checking
static PyObject* mouse_button_enum_class;
// Number of mouse buttons
static const int NUM_MOUSE_BUTTONS = 5;
};

135
src/PyTypeCache.cpp Normal file
View file

@ -0,0 +1,135 @@
// PyTypeCache.cpp - Thread-safe Python type caching implementation
#include "PyTypeCache.h"
// Static member definitions
std::atomic<PyTypeObject*> PyTypeCache::entity_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::grid_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::frame_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::caption_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::sprite_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::texture_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::color_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::vector_type{nullptr};
std::atomic<PyTypeObject*> PyTypeCache::font_type{nullptr};
std::atomic<bool> PyTypeCache::initialized{false};
std::mutex PyTypeCache::init_mutex;
PyTypeObject* PyTypeCache::cacheType(PyObject* module, const char* name,
std::atomic<PyTypeObject*>& cache) {
PyObject* type_obj = PyObject_GetAttrString(module, name);
if (!type_obj) {
PyErr_Format(PyExc_RuntimeError,
"PyTypeCache: Failed to get type '%s' from module", name);
return nullptr;
}
if (!PyType_Check(type_obj)) {
Py_DECREF(type_obj);
PyErr_Format(PyExc_TypeError,
"PyTypeCache: '%s' is not a type object", name);
return nullptr;
}
// Store in cache - we keep the reference permanently
// Using memory_order_release ensures the pointer is visible to other threads
// after they see initialized=true
cache.store((PyTypeObject*)type_obj, std::memory_order_release);
return (PyTypeObject*)type_obj;
}
bool PyTypeCache::initialize(PyObject* module) {
std::lock_guard<std::mutex> lock(init_mutex);
// Double-check pattern - might have been initialized while waiting for lock
if (initialized.load(std::memory_order_acquire)) {
return true;
}
// Cache all types
if (!cacheType(module, "Entity", entity_type)) return false;
if (!cacheType(module, "Grid", grid_type)) return false;
if (!cacheType(module, "Frame", frame_type)) return false;
if (!cacheType(module, "Caption", caption_type)) return false;
if (!cacheType(module, "Sprite", sprite_type)) return false;
if (!cacheType(module, "Texture", texture_type)) return false;
if (!cacheType(module, "Color", color_type)) return false;
if (!cacheType(module, "Vector", vector_type)) return false;
if (!cacheType(module, "Font", font_type)) return false;
// Mark as initialized - release ensures all stores above are visible
initialized.store(true, std::memory_order_release);
return true;
}
void PyTypeCache::finalize() {
std::lock_guard<std::mutex> lock(init_mutex);
if (!initialized.load(std::memory_order_acquire)) {
return;
}
// Release all cached references
auto release = [](std::atomic<PyTypeObject*>& cache) {
PyTypeObject* type = cache.exchange(nullptr, std::memory_order_acq_rel);
if (type) {
Py_DECREF(type);
}
};
release(entity_type);
release(grid_type);
release(frame_type);
release(caption_type);
release(sprite_type);
release(texture_type);
release(color_type);
release(vector_type);
release(font_type);
initialized.store(false, std::memory_order_release);
}
bool PyTypeCache::isInitialized() {
return initialized.load(std::memory_order_acquire);
}
// Type accessors - lock-free reads after initialization
// Using memory_order_acquire ensures we see the pointer stored during init
PyTypeObject* PyTypeCache::Entity() {
return entity_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Grid() {
return grid_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Frame() {
return frame_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Caption() {
return caption_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Sprite() {
return sprite_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Texture() {
return texture_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Color() {
return color_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Vector() {
return vector_type.load(std::memory_order_acquire);
}
PyTypeObject* PyTypeCache::Font() {
return font_type.load(std::memory_order_acquire);
}

64
src/PyTypeCache.h Normal file
View file

@ -0,0 +1,64 @@
#pragma once
// PyTypeCache.h - Thread-safe caching of Python type objects
//
// This module provides a centralized, thread-safe way to cache Python type
// references. It eliminates the refcount leaks from repeated
// PyObject_GetAttrString calls and is compatible with free-threading (PEP 703).
//
// Usage:
// PyTypeObject* entity_type = PyTypeCache::Entity();
// if (!entity_type) return NULL; // Error already set
//
// The cache is populated during module initialization and the types are
// held for the lifetime of the interpreter.
#include "Python.h"
#include <mutex>
#include <atomic>
class PyTypeCache {
public:
// Initialize the cache - call once during module init after types are ready
// Returns false and sets Python error on failure
static bool initialize(PyObject* module);
// Finalize - release references (call during module cleanup if needed)
static void finalize();
// Type accessors - return borrowed references (no DECREF needed)
// These are thread-safe and lock-free after initialization
static PyTypeObject* Entity();
static PyTypeObject* Grid();
static PyTypeObject* Frame();
static PyTypeObject* Caption();
static PyTypeObject* Sprite();
static PyTypeObject* Texture();
static PyTypeObject* Color();
static PyTypeObject* Vector();
static PyTypeObject* Font();
// Check if initialized
static bool isInitialized();
private:
// Cached type pointers - atomic for thread-safe reads
static std::atomic<PyTypeObject*> entity_type;
static std::atomic<PyTypeObject*> grid_type;
static std::atomic<PyTypeObject*> frame_type;
static std::atomic<PyTypeObject*> caption_type;
static std::atomic<PyTypeObject*> sprite_type;
static std::atomic<PyTypeObject*> texture_type;
static std::atomic<PyTypeObject*> color_type;
static std::atomic<PyTypeObject*> vector_type;
static std::atomic<PyTypeObject*> font_type;
// Initialization flag
static std::atomic<bool> initialized;
// Mutex for initialization (only used during init, not for reads)
static std::mutex init_mutex;
// Helper to fetch and cache a type
static PyTypeObject* cacheType(PyObject* module, const char* name,
std::atomic<PyTypeObject*>& cache);
};

1078
src/UIEntityCollection.cpp Normal file

File diff suppressed because it is too large Load diff

145
src/UIEntityCollection.h Normal file
View file

@ -0,0 +1,145 @@
#pragma once
// UIEntityCollection.h - Collection type for managing entities on a grid
//
// Extracted from UIGrid.cpp as part of code organization cleanup.
// This is a Python sequence/mapping type that wraps std::list<std::shared_ptr<UIEntity>>
// with grid-aware semantics (entities can only belong to one grid at a time).
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include <list>
#include <memory>
// Forward declarations
class UIEntity;
class UIGrid;
// Python object for EntityCollection
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data;
std::shared_ptr<UIGrid> grid;
} PyUIEntityCollectionObject;
// Python object for EntityCollection iterator
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data;
std::list<std::shared_ptr<UIEntity>>::iterator current; // Actual list iterator - O(1) increment
std::list<std::shared_ptr<UIEntity>>::iterator end; // End iterator for bounds check
int start_size; // For detecting modification during iteration
} PyUIEntityCollectionIterObject;
// UIEntityCollection - Python collection wrapper
class UIEntityCollection {
public:
// Python sequence protocol
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
// Collection methods
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
// Python type slots
static PyObject* repr(PyUIEntityCollectionObject* self);
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyObject* iter(PyUIEntityCollectionObject* self);
// Sequence methods
static Py_ssize_t len(PyUIEntityCollectionObject* self);
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other);
// Mapping methods (for slice support)
static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key);
static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value);
};
// UIEntityCollectionIter - Iterator for EntityCollection
class UIEntityCollectionIter {
public:
static int init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds);
static PyObject* next(PyUIEntityCollectionIterObject* self);
static PyObject* repr(PyUIEntityCollectionIterObject* self);
};
// Python type objects - defined in mcrfpydef namespace
namespace mcrfpydef {
// Iterator type
inline PyTypeObject PyUIEntityCollectionIterType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.UIEntityCollectionIter",
.tp_basicsize = sizeof(PyUIEntityCollectionIterObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIEntityCollectionIterObject* obj = (PyUIEntityCollectionIterObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIEntityCollectionIter::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterator for a collection of Entity objects"),
.tp_iter = PyObject_SelfIter,
.tp_iternext = (iternextfunc)UIEntityCollectionIter::next,
.tp_init = (initproc)UIEntityCollectionIter::init,
.tp_alloc = PyType_GenericAlloc,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyErr_SetString(PyExc_TypeError, "UIEntityCollectionIter cannot be instantiated directly");
return NULL;
}
};
// Collection type
inline PyTypeObject PyUIEntityCollectionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.EntityCollection",
.tp_basicsize = sizeof(PyUIEntityCollectionObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIEntityCollectionObject* obj = (PyUIEntityCollectionObject*)self;
obj->data.reset();
obj->grid.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIEntityCollection::repr,
.tp_as_sequence = &UIEntityCollection::sqmethods,
.tp_as_mapping = &UIEntityCollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of Entity objects.\n\n"
"EntityCollection manages entities that belong to a Grid. "
"Entities can only belong to one grid at a time - adding an entity "
"to a new grid automatically removes it from its previous grid.\n\n"
"Supports list-like operations: indexing, slicing, append, extend, "
"remove, pop, insert, index, count, and find.\n\n"
"Example:\n"
" grid.entities.append(entity)\n"
" player = grid.entities.find(name='player')\n"
" for entity in grid.entities:\n"
" print(entity.pos)"),
.tp_iter = (getiterfunc)UIEntityCollection::iter,
.tp_methods = UIEntityCollection::methods,
.tp_init = (initproc)UIEntityCollection::init,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required.");
return NULL;
}
};
} // namespace mcrfpydef

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@
#include "GridLayers.h"
#include "GridChunk.h"
#include "SpatialHash.h"
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
class UIGrid: public UIDrawable
{
@ -180,6 +181,8 @@ public:
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyMappingMethods mpmethods; // For grid[x, y] subscript access
static PyObject* subscript(PyUIGridObject* self, PyObject* key); // __getitem__
static PyObject* get_entities(PyUIGridObject* self, void* closure);
static PyObject* get_children(PyUIGridObject* self, void* closure);
static PyObject* repr(PyUIGridObject* self);
@ -200,54 +203,7 @@ public:
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
};
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data;
std::shared_ptr<UIGrid> grid;
} PyUIEntityCollectionObject;
class UIEntityCollection {
public:
static PySequenceMethods sqmethods;
static PyMappingMethods mpmethods;
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
static PyObject* pop(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* insert(PyUIEntityCollectionObject* self, PyObject* args);
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* find(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyObject* repr(PyUIEntityCollectionObject* self);
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
static PyObject* iter(PyUIEntityCollectionObject* self);
static Py_ssize_t len(PyUIEntityCollectionObject* self);
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value);
static int contains(PyUIEntityCollectionObject* self, PyObject* value);
static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other);
static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key);
static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value);
};
typedef struct {
PyObject_HEAD
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> data;
std::list<std::shared_ptr<UIEntity>>::iterator current; // Actual list iterator - O(1) increment
std::list<std::shared_ptr<UIEntity>>::iterator end; // End iterator for bounds check
int start_size; // For detecting modification during iteration
} PyUIEntityCollectionIterObject;
class UIEntityCollectionIter {
public:
static int init(PyUIEntityCollectionIterObject* self, PyObject* args, PyObject* kwds);
static PyObject* next(PyUIEntityCollectionIterObject* self);
static PyObject* repr(PyUIEntityCollectionIterObject* self);
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
};
// UIEntityCollection types are now in UIEntityCollection.h
// Forward declaration of methods array
extern PyMethodDef UIGrid_all_methods[];
@ -268,11 +224,8 @@ namespace mcrfpydef {
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
//TODO - PyUIGrid REPR def:
.tp_repr = (reprfunc)UIGrid::repr,
//.tp_hash = NULL,
//.tp_iter
//.tp_iternext
.tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
"A grid-based UI element for tile-based rendering and entity management.\n\n"
@ -330,61 +283,6 @@ namespace mcrfpydef {
}
};
// #189 - Use inline instead of static to ensure single instance across translation units
inline PyTypeObject PyUIEntityCollectionIterType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.UIEntityCollectionIter",
.tp_basicsize = sizeof(PyUIEntityCollectionIterObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIEntityCollectionIterObject* obj = (PyUIEntityCollectionIterObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIEntityCollectionIter::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterator for a collection of UI objects"),
.tp_iter = PyObject_SelfIter,
.tp_iternext = (iternextfunc)UIEntityCollectionIter::next,
//.tp_getset = UIEntityCollection::getset,
.tp_init = (initproc)UIEntityCollectionIter::init, // just raise an exception
.tp_alloc = PyType_GenericAlloc,
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
PyErr_SetString(PyExc_TypeError, "UICollection cannot be instantiated: a C++ data source is required.");
return NULL;
}
};
// #189 - Use inline instead of static to ensure single instance across translation units
inline PyTypeObject PyUIEntityCollectionType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.EntityCollection",
.tp_basicsize = sizeof(PyUIEntityCollectionObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)[](PyObject* self)
{
PyUIEntityCollectionObject* obj = (PyUIEntityCollectionObject*)self;
obj->data.reset();
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIEntityCollection::repr,
.tp_as_sequence = &UIEntityCollection::sqmethods,
.tp_as_mapping = &UIEntityCollection::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Iterable, indexable collection of Entities"),
.tp_iter = (getiterfunc)UIEntityCollection::iter,
.tp_methods = UIEntityCollection::methods, // append, remove
//.tp_getset = UIEntityCollection::getset,
.tp_init = (initproc)UIEntityCollection::init, // just raise an exception
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
{
// Does PyUIEntityCollectionType need __new__ if it's not supposed to be instantiable by the user?
// Should I just raise an exception? Or is the uninitialized shared_ptr enough of a blocker?
PyErr_SetString(PyExc_TypeError, "EntityCollection cannot be instantiated: a C++ data source is required.");
return NULL;
}
};
// EntityCollection types moved to UIEntityCollection.h
}

View file

@ -4,11 +4,212 @@ Core game engine interface for creating roguelike games with Python.
"""
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
from enum import IntEnum
# Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid', 'Line', 'Circle', 'Arc']
Transition = Union[str, None]
# Enums
class Key(IntEnum):
"""Keyboard key codes.
These enum values compare equal to their legacy string equivalents
for backwards compatibility:
Key.ESCAPE == 'Escape' # True
Key.LEFT_SHIFT == 'LShift' # True
"""
# Letters
A = 0
B = 1
C = 2
D = 3
E = 4
F = 5
G = 6
H = 7
I = 8
J = 9
K = 10
L = 11
M = 12
N = 13
O = 14
P = 15
Q = 16
R = 17
S = 18
T = 19
U = 20
V = 21
W = 22
X = 23
Y = 24
Z = 25
# Number row
NUM_0 = 26
NUM_1 = 27
NUM_2 = 28
NUM_3 = 29
NUM_4 = 30
NUM_5 = 31
NUM_6 = 32
NUM_7 = 33
NUM_8 = 34
NUM_9 = 35
# Control keys
ESCAPE = 36
LEFT_CONTROL = 37
LEFT_SHIFT = 38
LEFT_ALT = 39
LEFT_SYSTEM = 40
RIGHT_CONTROL = 41
RIGHT_SHIFT = 42
RIGHT_ALT = 43
RIGHT_SYSTEM = 44
MENU = 45
# Punctuation
LEFT_BRACKET = 46
RIGHT_BRACKET = 47
SEMICOLON = 48
COMMA = 49
PERIOD = 50
APOSTROPHE = 51
SLASH = 52
BACKSLASH = 53
GRAVE = 54
EQUAL = 55
HYPHEN = 56
# Whitespace/editing
SPACE = 57
ENTER = 58
BACKSPACE = 59
TAB = 60
# Navigation
PAGE_UP = 61
PAGE_DOWN = 62
END = 63
HOME = 64
INSERT = 65
DELETE = 66
# Numpad operators
ADD = 67
SUBTRACT = 68
MULTIPLY = 69
DIVIDE = 70
# Arrows
LEFT = 71
RIGHT = 72
UP = 73
DOWN = 74
# Numpad numbers
NUMPAD_0 = 75
NUMPAD_1 = 76
NUMPAD_2 = 77
NUMPAD_3 = 78
NUMPAD_4 = 79
NUMPAD_5 = 80
NUMPAD_6 = 81
NUMPAD_7 = 82
NUMPAD_8 = 83
NUMPAD_9 = 84
# Function keys
F1 = 85
F2 = 86
F3 = 87
F4 = 88
F5 = 89
F6 = 90
F7 = 91
F8 = 92
F9 = 93
F10 = 94
F11 = 95
F12 = 96
F13 = 97
F14 = 98
F15 = 99
# Misc
PAUSE = 100
UNKNOWN = -1
class MouseButton(IntEnum):
"""Mouse button codes.
These enum values compare equal to their legacy string equivalents
for backwards compatibility:
MouseButton.LEFT == 'left' # True
MouseButton.RIGHT == 'right' # True
"""
LEFT = 0
RIGHT = 1
MIDDLE = 2
X1 = 3
X2 = 4
class InputState(IntEnum):
"""Input event states (pressed/released).
These enum values compare equal to their legacy string equivalents
for backwards compatibility:
InputState.PRESSED == 'start' # True
InputState.RELEASED == 'end' # True
"""
PRESSED = 0
RELEASED = 1
class Easing(IntEnum):
"""Easing functions for animations."""
LINEAR = 0
EASE_IN = 1
EASE_OUT = 2
EASE_IN_OUT = 3
EASE_IN_QUAD = 4
EASE_OUT_QUAD = 5
EASE_IN_OUT_QUAD = 6
EASE_IN_CUBIC = 7
EASE_OUT_CUBIC = 8
EASE_IN_OUT_CUBIC = 9
EASE_IN_QUART = 10
EASE_OUT_QUART = 11
EASE_IN_OUT_QUART = 12
EASE_IN_SINE = 13
EASE_OUT_SINE = 14
EASE_IN_OUT_SINE = 15
EASE_IN_EXPO = 16
EASE_OUT_EXPO = 17
EASE_IN_OUT_EXPO = 18
EASE_IN_CIRC = 19
EASE_OUT_CIRC = 20
EASE_IN_OUT_CIRC = 21
EASE_IN_ELASTIC = 22
EASE_OUT_ELASTIC = 23
EASE_IN_OUT_ELASTIC = 24
EASE_IN_BACK = 25
EASE_OUT_BACK = 26
EASE_IN_OUT_BACK = 27
EASE_IN_BOUNCE = 28
EASE_OUT_BOUNCE = 29
EASE_IN_OUT_BOUNCE = 30
class FOV(IntEnum):
"""Field of view algorithms for visibility calculations."""
BASIC = 0
DIAMOND = 1
SHADOW = 2
PERMISSIVE_0 = 3
PERMISSIVE_1 = 4
PERMISSIVE_2 = 5
PERMISSIVE_3 = 6
PERMISSIVE_4 = 7
PERMISSIVE_5 = 8
PERMISSIVE_6 = 9
PERMISSIVE_7 = 10
PERMISSIVE_8 = 11
RESTRICTIVE = 12
SYMMETRIC_SHADOWCAST = 13
# Classes
class Color:

View file

@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Test Key, MouseButton, and InputState enum functionality.
Tests the new input-related enums that provide type-safe alternatives to
string-based key codes, mouse buttons, and event states.
"""
import mcrfpy
import sys
def test_key_enum():
"""Test Key enum members and backwards compatibility."""
print("Testing Key enum...")
# Test that enum exists and has expected members
assert hasattr(mcrfpy, 'Key'), "mcrfpy.Key should exist"
assert hasattr(mcrfpy.Key, 'A'), "Key.A should exist"
assert hasattr(mcrfpy.Key, 'ESCAPE'), "Key.ESCAPE should exist"
assert hasattr(mcrfpy.Key, 'LEFT_SHIFT'), "Key.LEFT_SHIFT should exist"
# Test int values
assert int(mcrfpy.Key.A) == 0, "Key.A should be 0"
assert int(mcrfpy.Key.ESCAPE) == 36, "Key.ESCAPE should be 36"
# Test backwards compatibility with legacy strings
assert mcrfpy.Key.A == "A", "Key.A should equal 'A'"
assert mcrfpy.Key.ESCAPE == "Escape", "Key.ESCAPE should equal 'Escape'"
assert mcrfpy.Key.LEFT_SHIFT == "LShift", "Key.LEFT_SHIFT should equal 'LShift'"
assert mcrfpy.Key.RIGHT_CONTROL == "RControl", "Key.RIGHT_CONTROL should equal 'RControl'"
assert mcrfpy.Key.NUM_1 == "Num1", "Key.NUM_1 should equal 'Num1'"
assert mcrfpy.Key.NUMPAD_5 == "Numpad5", "Key.NUMPAD_5 should equal 'Numpad5'"
assert mcrfpy.Key.F12 == "F12", "Key.F12 should equal 'F12'"
assert mcrfpy.Key.SPACE == "Space", "Key.SPACE should equal 'Space'"
# Test that enum name also matches
assert mcrfpy.Key.ESCAPE == "ESCAPE", "Key.ESCAPE should also equal 'ESCAPE'"
print(" All Key tests passed")
def test_mouse_button_enum():
"""Test MouseButton enum members and backwards compatibility."""
print("Testing MouseButton enum...")
# Test that enum exists and has expected members
assert hasattr(mcrfpy, 'MouseButton'), "mcrfpy.MouseButton should exist"
assert hasattr(mcrfpy.MouseButton, 'LEFT'), "MouseButton.LEFT should exist"
assert hasattr(mcrfpy.MouseButton, 'RIGHT'), "MouseButton.RIGHT should exist"
assert hasattr(mcrfpy.MouseButton, 'MIDDLE'), "MouseButton.MIDDLE should exist"
# Test int values
assert int(mcrfpy.MouseButton.LEFT) == 0, "MouseButton.LEFT should be 0"
assert int(mcrfpy.MouseButton.RIGHT) == 1, "MouseButton.RIGHT should be 1"
assert int(mcrfpy.MouseButton.MIDDLE) == 2, "MouseButton.MIDDLE should be 2"
# Test backwards compatibility with legacy strings
assert mcrfpy.MouseButton.LEFT == "left", "MouseButton.LEFT should equal 'left'"
assert mcrfpy.MouseButton.RIGHT == "right", "MouseButton.RIGHT should equal 'right'"
assert mcrfpy.MouseButton.MIDDLE == "middle", "MouseButton.MIDDLE should equal 'middle'"
assert mcrfpy.MouseButton.X1 == "x1", "MouseButton.X1 should equal 'x1'"
assert mcrfpy.MouseButton.X2 == "x2", "MouseButton.X2 should equal 'x2'"
# Test that enum name also matches
assert mcrfpy.MouseButton.LEFT == "LEFT", "MouseButton.LEFT should also equal 'LEFT'"
print(" All MouseButton tests passed")
def test_input_state_enum():
"""Test InputState enum members and backwards compatibility."""
print("Testing InputState enum...")
# Test that enum exists and has expected members
assert hasattr(mcrfpy, 'InputState'), "mcrfpy.InputState should exist"
assert hasattr(mcrfpy.InputState, 'PRESSED'), "InputState.PRESSED should exist"
assert hasattr(mcrfpy.InputState, 'RELEASED'), "InputState.RELEASED should exist"
# Test int values
assert int(mcrfpy.InputState.PRESSED) == 0, "InputState.PRESSED should be 0"
assert int(mcrfpy.InputState.RELEASED) == 1, "InputState.RELEASED should be 1"
# Test backwards compatibility with legacy strings
assert mcrfpy.InputState.PRESSED == "start", "InputState.PRESSED should equal 'start'"
assert mcrfpy.InputState.RELEASED == "end", "InputState.RELEASED should equal 'end'"
# Test that enum name also matches
assert mcrfpy.InputState.PRESSED == "PRESSED", "InputState.PRESSED should also equal 'PRESSED'"
assert mcrfpy.InputState.RELEASED == "RELEASED", "InputState.RELEASED should also equal 'RELEASED'"
print(" All InputState tests passed")
def test_enum_repr():
"""Test that enum repr/str work correctly."""
print("Testing enum repr/str...")
# Test repr
assert "Key.ESCAPE" in repr(mcrfpy.Key.ESCAPE), f"repr should contain 'Key.ESCAPE', got {repr(mcrfpy.Key.ESCAPE)}"
assert "MouseButton.LEFT" in repr(mcrfpy.MouseButton.LEFT), f"repr should contain 'MouseButton.LEFT'"
assert "InputState.PRESSED" in repr(mcrfpy.InputState.PRESSED), f"repr should contain 'InputState.PRESSED'"
# Test str
assert str(mcrfpy.Key.ESCAPE) == "ESCAPE", f"str(Key.ESCAPE) should be 'ESCAPE', got {str(mcrfpy.Key.ESCAPE)}"
assert str(mcrfpy.MouseButton.LEFT) == "LEFT", f"str(MouseButton.LEFT) should be 'LEFT'"
assert str(mcrfpy.InputState.PRESSED) == "PRESSED", f"str(InputState.PRESSED) should be 'PRESSED'"
print(" All repr/str tests passed")
def main():
"""Run all enum tests."""
print("=" * 50)
print("Input Enum Unit Tests")
print("=" * 50)
try:
test_key_enum()
test_mouse_button_enum()
test_input_state_enum()
test_enum_repr()
print()
print("=" * 50)
print("All tests PASSED")
print("=" * 50)
sys.exit(0)
except AssertionError as e:
print(f"\nTest FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"\nUnexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()