Input Enums instead of strings.

This commit is contained in:
John McCardle 2026-01-10 21:31:20 -05:00
commit 9eacedc624
9 changed files with 1205 additions and 0 deletions

View file

@ -13,6 +13,9 @@
#include "PyFOV.h" #include "PyFOV.h"
#include "PyTransition.h" #include "PyTransition.h"
#include "PyEasing.h" #include "PyEasing.h"
#include "PyKey.h"
#include "PyMouseButton.h"
#include "PyInputState.h"
#include "PySound.h" #include "PySound.h"
#include "PyMusic.h" #include "PyMusic.h"
#include "PyKeyboard.h" #include "PyKeyboard.h"
@ -536,6 +539,27 @@ PyObject* PyInit_mcrfpy()
PyErr_Clear(); 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 // Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module(); PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) { if (automation_module != NULL) {

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;
};

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 typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
from enum import IntEnum
# Type aliases # Type aliases
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid', 'Line', 'Circle', 'Arc'] UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid', 'Line', 'Circle', 'Arc']
Transition = Union[str, None] 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 # Classes
class Color: 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()