Squashed commit of the following: [alpha_presentable]
Author: John McCardle <mccardle.john@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
commit dc47f2474c7b2642d368f9772894aed857527807
the UIEntity rant
commit 673ca8e1b089ea670257fc04ae1a676ed95a40ed
I forget when these tests were written, but I want them in the squash merge
commit 70c71565c684fa96e222179271ecb13a156d80ad
Fix UI object segfault by switching from managed to manual weakref management
The UI types (Frame, Caption, Sprite, Grid, Entity) were using
Py_TPFLAGS_MANAGED_WEAKREF while also trying to manually create weakrefs
for the PythonObjectCache. This is fundamentally incompatible - when
Python manages weakrefs internally, PyWeakref_NewRef() cannot access the
weakref list properly, causing segfaults.
Changed all UI types to use manual weakref management (like PyTimer):
- Restored weakreflist field in all UI type structures
- Removed Py_TPFLAGS_MANAGED_WEAKREF from all UI type flags
- Added tp_weaklistoffset for all UI types in module initialization
- Initialize weakreflist=NULL in tp_new and init methods
- Call PyObject_ClearWeakRefs() in dealloc functions
This allows the PythonObjectCache to continue working correctly,
maintaining Python object identity for C++ objects across the boundary.
Fixes segfault when creating UI objects (e.g., Caption, Grid) that was
preventing tutorial scripts from running.
This is the bulk of the required behavior for Issue #126.
that issure isn't ready for closure yet; several other sub-issues left.
closes #110
mention issue #109 - resolves some __init__ related nuisances
commit 3dce3ec539ae99e32d869007bf3f49d03e4e2f89
Refactor timer system for cleaner architecture and enhanced functionality
Major improvements to the timer system:
- Unified all timer logic in the Timer class (C++)
- Removed PyTimerCallable subclass, now using PyCallable directly
- Timer objects are now passed to callbacks as first argument
- Added 'once' parameter for one-shot timers that auto-stop
- Implemented proper PythonObjectCache integration with weakref support
API enhancements:
- New callback signature: callback(timer, runtime) instead of just (runtime)
- Timer objects expose: name, interval, remaining, paused, active, once properties
- Methods: pause(), resume(), cancel(), restart()
- Comprehensive documentation with examples
- Enhanced repr showing timer state (active/paused/once/remaining time)
This cleanup follows the UIEntity/PyUIEntity pattern and makes the timer
system more Pythonic while maintaining backward compatibility through
the legacy setTimer/delTimer API.
closes #121
commit 145834cfc31b8dabc4cb3591b9cb4ed99fc8b964
Implement Python object cache to preserve derived types in collections
Add a global cache system that maintains weak references to Python objects,
ensuring that derived Python classes maintain their identity when stored in
and retrieved from C++ collections.
Key changes:
- Add PythonObjectCache singleton with serial number system
- Each cacheable object (UIDrawable, UIEntity, Timer, Animation) gets unique ID
- Cache stores weak references to prevent circular reference memory leaks
- Update all UI type definitions to support weak references (Py_TPFLAGS_MANAGED_WEAKREF)
- Enable subclassing for all UI types (Py_TPFLAGS_BASETYPE)
- Collections check cache before creating new Python wrappers
- Register objects in cache during __init__ methods
- Clean up cache entries in C++ destructors
This ensures that Python code like:
```python
class MyFrame(mcrfpy.Frame):
def __init__(self):
super().__init__()
self.custom_data = "preserved"
frame = MyFrame()
scene.ui.append(frame)
retrieved = scene.ui[0] # Same MyFrame instance with custom_data intact
```
Works correctly, with retrieved maintaining the derived type and custom attributes.
Closes #112
commit c5e7e8e298
Update test demos for new Python API and entity system
- Update all text input demos to use new Entity constructor signature
- Fix pathfinding showcase to work with new entity position handling
- Remove entity_waypoints tracking in favor of simplified movement
- Delete obsolete exhaustive_api_demo.py (superseded by newer demos)
- Adjust entity creation calls to match Entity((x, y), texture, sprite_index) pattern
commit 6d29652ae7
Update animation demo suite with crash fixes and improvements
- Add warnings about AnimationManager segfault bug in sizzle_reel_final.py
- Create sizzle_reel_final_fixed.py that works around the crash by hiding objects instead of removing them
- Increase font sizes for better visibility in demos
- Extend demo durations for better showcase of animations
- Remove debug prints from animation_sizzle_reel_working.py
- Minor cleanup and improvements to all animation demos
commit a010e5fa96
Update game scripts for new Python API
- Convert entity position access from tuple to x/y properties
- Update caption size property to font_size
- Fix grid boundary checks to use grid_size instead of exceptions
- Clean up demo timer on menu exit to prevent callbacks
These changes adapt the game scripts to work with the new standardized
Python API constructors and property names.
commit 9c8d6c4591
Fix click event z-order handling in PyScene
Changed click detection to properly respect z-index by:
- Sorting ui_elements in-place when needed (same as render order)
- Using reverse iterators to check highest z-index elements first
- This ensures top-most elements receive clicks before lower ones
commit dcd1b0ca33
Add roguelike tutorial implementation files
Implement Parts 0-2 of the classic roguelike tutorial adapted for McRogueFace:
- Part 0: Basic grid setup and tile rendering
- Part 1: Drawing '@' symbol and basic movement
- Part 1b: Variant with sprite-based player
- Part 2: Entity system and NPC implementation with three movement variants:
- part_2.py: Standard implementation
- part_2-naive.py: Naive movement approach
- part_2-onemovequeued.py: Queued movement system
Includes tutorial assets:
- tutorial2.png: Tileset for dungeon tiles
- tutorial_hero.png: Player sprite sheet
commit 6813fb5129
Standardize Python API constructors and remove PyArgHelpers
- Remove PyArgHelpers.h and all macro-based argument parsing
- Convert all UI class constructors to use PyArg_ParseTupleAndKeywords
- Standardize constructor signatures across UICaption, UIEntity, UIFrame, UIGrid, and UISprite
- Replace PYARGHELPER_SINGLE/MULTI macros with explicit argument parsing
- Improve error messages and argument validation
- Maintain backward compatibility with existing Python code
This change improves code maintainability and consistency across the Python API.
commit 6f67fbb51e
Fix animation callback crashes from iterator invalidation (#119)
Resolved segfaults caused by creating new animations from within
animation callbacks. The issue was iterator invalidation in
AnimationManager::update() when callbacks modified the active
animations vector.
Changes:
- Add deferred animation queue to AnimationManager
- New animations created during update are queued and added after
- Set isUpdating flag to track when in update loop
- Properly handle Animation destructor during callback execution
- Add clearCallback() method for safe cleanup scenarios
This fixes the "free(): invalid pointer" and "malloc(): unaligned
fastbin chunk detected" errors that occurred with rapid animation
creation in callbacks.
commit eb88c7b3aa
Add animation completion callbacks (#119)
Implement callbacks that fire when animations complete, enabling direct
causality between animation end and game state changes. This eliminates
race conditions from parallel timer workarounds.
- Add optional callback parameter to Animation constructor
- Callbacks execute synchronously when animation completes
- Proper Python reference counting with GIL safety
- Callbacks receive (anim, target) parameters (currently None)
- Exception handling prevents crashes from Python errors
Example usage:
```python
def on_complete(anim, target):
player_moving = False
anim = mcrfpy.Animation("x", 300.0, 1.0, "easeOut", callback=on_complete)
anim.start(player)
```
closes #119
commit 9fb428dd01
Update ROADMAP with GitHub issue numbers (#111-#125)
Added issue numbers from GitHub tracker to roadmap items:
- #111: Grid Click Events Broken in Headless
- #112: Object Splitting Bug (Python type preservation)
- #113: Batch Operations for Grid
- #114: CellView API
- #115: SpatialHash Implementation
- #116: Dirty Flag System
- #117: Memory Pool for Entities
- #118: Scene as Drawable
- #119: Animation Completion Callbacks
- #120: Animation Property Locking
- #121: Timer Object System
- #122: Parent-Child UI System
- #123: Grid Subgrid System
- #124: Grid Point Animation
- #125: GitHub Issues Automation
Also updated existing references:
- #101/#110: Constructor standardization
- #109: Vector class indexing
Note: Tutorial-specific items and Python-implementable features
(input queue, collision reservation) are not tracked as engine issues.
commit 062e4dadc4
Fix animation segfaults with RAII weak_ptr implementation
Resolved two critical segmentation faults in AnimationManager:
1. Race condition when creating multiple animations in timer callbacks
2. Exit crash when animations outlive their target objects
Changes:
- Replace raw pointers with std::weak_ptr for automatic target invalidation
- Add Animation::complete() to jump animations to final value
- Add Animation::hasValidTarget() to check if target still exists
- Update AnimationManager to auto-remove invalid animations
- Add AnimationManager::clear() call to GameEngine::cleanup()
- Update Python bindings to pass shared_ptr instead of raw pointers
This ensures animations can never reference destroyed objects, following
proper RAII principles. Tested with sizzle_reel_final.py and stress
tests creating/destroying hundreds of animated objects.
commit 98fc49a978
Directory structure cleanup and organization overhaul
This commit is contained in:
parent
1a143982e1
commit
f4343e1e82
163 changed files with 12812 additions and 5441 deletions
|
|
@ -1,6 +1,9 @@
|
|||
#include "Animation.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIEntity.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
|
@ -9,75 +12,105 @@
|
|||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
// Forward declaration of PyAnimation type
|
||||
namespace mcrfpydef {
|
||||
extern PyTypeObject PyAnimationType;
|
||||
}
|
||||
|
||||
// Animation implementation
|
||||
Animation::Animation(const std::string& targetProperty,
|
||||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc,
|
||||
bool delta)
|
||||
bool delta,
|
||||
PyObject* callback)
|
||||
: targetProperty(targetProperty)
|
||||
, targetValue(targetValue)
|
||||
, duration(duration)
|
||||
, easingFunc(easingFunc)
|
||||
, delta(delta)
|
||||
, pythonCallback(callback)
|
||||
{
|
||||
// Increase reference count for Python callback
|
||||
if (pythonCallback) {
|
||||
Py_INCREF(pythonCallback);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(UIDrawable* target) {
|
||||
currentTarget = target;
|
||||
Animation::~Animation() {
|
||||
// Decrease reference count for Python callback if we still own it
|
||||
PyObject* callback = pythonCallback;
|
||||
if (callback) {
|
||||
pythonCallback = nullptr;
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
Py_DECREF(callback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
// Clean up cache entry
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::start(std::shared_ptr<UIDrawable> target) {
|
||||
if (!target) return;
|
||||
|
||||
targetWeak = target;
|
||||
elapsed = 0.0f;
|
||||
callbackTriggered = false; // Reset callback state
|
||||
|
||||
// Capture startValue from target based on targetProperty
|
||||
if (!currentTarget) return;
|
||||
|
||||
// Try to get the current value based on the expected type
|
||||
std::visit([this](const auto& targetVal) {
|
||||
// Capture start value from target
|
||||
std::visit([this, &target](const auto& targetVal) {
|
||||
using T = std::decay_t<decltype(targetVal)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||
// For sprite animation, get current sprite index
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
sf::Color value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
sf::Vector2f value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
std::string value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
}, targetValue);
|
||||
}
|
||||
|
||||
void Animation::startEntity(UIEntity* target) {
|
||||
currentEntityTarget = target;
|
||||
currentTarget = nullptr; // Clear drawable target
|
||||
void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
||||
if (!target) return;
|
||||
|
||||
entityTargetWeak = target;
|
||||
elapsed = 0.0f;
|
||||
callbackTriggered = false; // Reset callback state
|
||||
|
||||
// Capture the starting value from the entity
|
||||
std::visit([this, target](const auto& val) {
|
||||
|
|
@ -99,8 +132,49 @@ void Animation::startEntity(UIEntity* target) {
|
|||
}, targetValue);
|
||||
}
|
||||
|
||||
bool Animation::hasValidTarget() const {
|
||||
return !targetWeak.expired() || !entityTargetWeak.expired();
|
||||
}
|
||||
|
||||
void Animation::clearCallback() {
|
||||
// Safely clear the callback when PyAnimation is being destroyed
|
||||
PyObject* callback = pythonCallback;
|
||||
if (callback) {
|
||||
pythonCallback = nullptr;
|
||||
callbackTriggered = true; // Prevent future triggering
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
Py_DECREF(callback);
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
}
|
||||
|
||||
void Animation::complete() {
|
||||
// Jump to end of animation
|
||||
elapsed = duration;
|
||||
|
||||
// Apply final value
|
||||
if (auto target = targetWeak.lock()) {
|
||||
AnimationValue finalValue = interpolate(1.0f);
|
||||
applyValue(target.get(), finalValue);
|
||||
}
|
||||
else if (auto entity = entityTargetWeak.lock()) {
|
||||
AnimationValue finalValue = interpolate(1.0f);
|
||||
applyValue(entity.get(), finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
bool Animation::update(float deltaTime) {
|
||||
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
|
||||
// Try to lock weak_ptr to get shared_ptr
|
||||
std::shared_ptr<UIDrawable> target = targetWeak.lock();
|
||||
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
|
||||
|
||||
// If both are null, target was destroyed
|
||||
if (!target && !entity) {
|
||||
return false; // Remove this animation
|
||||
}
|
||||
|
||||
if (isComplete()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -114,39 +188,18 @@ bool Animation::update(float deltaTime) {
|
|||
// Get interpolated value
|
||||
AnimationValue currentValue = interpolate(easedT);
|
||||
|
||||
// Apply currentValue to target (either drawable or entity)
|
||||
std::visit([this](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
|
||||
if (currentTarget) {
|
||||
// Handle UIDrawable targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
}
|
||||
else if (currentEntityTarget) {
|
||||
// Handle UIEntity targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}
|
||||
}, currentValue);
|
||||
// Apply to whichever target is valid
|
||||
if (target) {
|
||||
applyValue(target.get(), currentValue);
|
||||
} else if (entity) {
|
||||
applyValue(entity.get(), currentValue);
|
||||
}
|
||||
|
||||
// Trigger callback when animation completes
|
||||
// Check pythonCallback again in case it was cleared during update
|
||||
if (isComplete() && !callbackTriggered && pythonCallback) {
|
||||
triggerCallback();
|
||||
}
|
||||
|
||||
return !isComplete();
|
||||
}
|
||||
|
|
@ -254,6 +307,77 @@ AnimationValue Animation::interpolate(float t) const {
|
|||
}, targetValue);
|
||||
}
|
||||
|
||||
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
|
||||
if (!target) return;
|
||||
|
||||
std::visit([this, target](const auto& val) {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
target->setProperty(targetProperty, val);
|
||||
}
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
||||
if (!entity) return;
|
||||
|
||||
std::visit([this, entity](const auto& val) {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
entity->setProperty(targetProperty, val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
entity->setProperty(targetProperty, val);
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}, value);
|
||||
}
|
||||
|
||||
void Animation::triggerCallback() {
|
||||
if (!pythonCallback) return;
|
||||
|
||||
// Ensure we only trigger once
|
||||
if (callbackTriggered) return;
|
||||
callbackTriggered = true;
|
||||
|
||||
PyGILState_STATE gstate = PyGILState_Ensure();
|
||||
|
||||
// TODO: In future, create PyAnimation wrapper for this animation
|
||||
// For now, pass None for both parameters
|
||||
PyObject* args = PyTuple_New(2);
|
||||
Py_INCREF(Py_None);
|
||||
Py_INCREF(Py_None);
|
||||
PyTuple_SetItem(args, 0, Py_None); // animation parameter
|
||||
PyTuple_SetItem(args, 1, Py_None); // target parameter
|
||||
|
||||
PyObject* result = PyObject_CallObject(pythonCallback, args);
|
||||
Py_DECREF(args);
|
||||
|
||||
if (!result) {
|
||||
// Print error but don't crash
|
||||
PyErr_Print();
|
||||
PyErr_Clear(); // Clear the error state
|
||||
} else {
|
||||
Py_DECREF(result);
|
||||
}
|
||||
|
||||
PyGILState_Release(gstate);
|
||||
}
|
||||
|
||||
// Easing functions implementation
|
||||
namespace EasingFunctions {
|
||||
|
||||
|
|
@ -502,26 +626,50 @@ AnimationManager& AnimationManager::getInstance() {
|
|||
}
|
||||
|
||||
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
||||
activeAnimations.push_back(animation);
|
||||
if (animation && animation->hasValidTarget()) {
|
||||
if (isUpdating) {
|
||||
// Defer adding during update to avoid iterator invalidation
|
||||
pendingAnimations.push_back(animation);
|
||||
} else {
|
||||
activeAnimations.push_back(animation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationManager::update(float deltaTime) {
|
||||
for (auto& anim : activeAnimations) {
|
||||
anim->update(deltaTime);
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void AnimationManager::cleanup() {
|
||||
// Set flag to defer new animations
|
||||
isUpdating = true;
|
||||
|
||||
// Remove completed or invalid animations
|
||||
activeAnimations.erase(
|
||||
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
||||
[](const std::shared_ptr<Animation>& anim) {
|
||||
return anim->isComplete();
|
||||
[deltaTime](std::shared_ptr<Animation>& anim) {
|
||||
return !anim || !anim->update(deltaTime);
|
||||
}),
|
||||
activeAnimations.end()
|
||||
);
|
||||
|
||||
// Clear update flag
|
||||
isUpdating = false;
|
||||
|
||||
// Add any animations that were created during update
|
||||
if (!pendingAnimations.empty()) {
|
||||
activeAnimations.insert(activeAnimations.end(),
|
||||
pendingAnimations.begin(),
|
||||
pendingAnimations.end());
|
||||
pendingAnimations.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void AnimationManager::clear() {
|
||||
|
||||
void AnimationManager::clear(bool completeAnimations) {
|
||||
if (completeAnimations) {
|
||||
// Complete all animations before clearing
|
||||
for (auto& anim : activeAnimations) {
|
||||
if (anim) {
|
||||
anim->complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
activeAnimations.clear();
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <variant>
|
||||
#include <vector>
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include "Python.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIDrawable;
|
||||
|
|
@ -36,13 +37,20 @@ public:
|
|||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc = EasingFunctions::linear,
|
||||
bool delta = false);
|
||||
bool delta = false,
|
||||
PyObject* callback = nullptr);
|
||||
|
||||
// Destructor - cleanup Python callback reference
|
||||
~Animation();
|
||||
|
||||
// Apply this animation to a drawable
|
||||
void start(UIDrawable* target);
|
||||
void start(std::shared_ptr<UIDrawable> target);
|
||||
|
||||
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
||||
void startEntity(UIEntity* target);
|
||||
void startEntity(std::shared_ptr<UIEntity> target);
|
||||
|
||||
// Complete the animation immediately (jump to final value)
|
||||
void complete();
|
||||
|
||||
// Update animation (called each frame)
|
||||
// Returns true if animation is still running, false if complete
|
||||
|
|
@ -51,6 +59,12 @@ public:
|
|||
// Get current interpolated value
|
||||
AnimationValue getCurrentValue() const;
|
||||
|
||||
// Check if animation has valid target
|
||||
bool hasValidTarget() const;
|
||||
|
||||
// Clear the callback (called when PyAnimation is deallocated)
|
||||
void clearCallback();
|
||||
|
||||
// Animation properties
|
||||
std::string getTargetProperty() const { return targetProperty; }
|
||||
float getDuration() const { return duration; }
|
||||
|
|
@ -67,11 +81,27 @@ private:
|
|||
EasingFunction easingFunc; // Easing function to use
|
||||
bool delta; // If true, targetValue is relative to start
|
||||
|
||||
UIDrawable* currentTarget = nullptr; // Current target being animated
|
||||
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
|
||||
// RAII: Use weak_ptr for safe target tracking
|
||||
std::weak_ptr<UIDrawable> targetWeak;
|
||||
std::weak_ptr<UIEntity> entityTargetWeak;
|
||||
|
||||
// Callback support
|
||||
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
|
||||
bool callbackTriggered = false; // Ensure callback only fires once
|
||||
PyObject* pyAnimationWrapper = nullptr; // Weak reference to PyAnimation if created from Python
|
||||
|
||||
// Python object cache support
|
||||
uint64_t serial_number = 0;
|
||||
|
||||
// Helper to interpolate between values
|
||||
AnimationValue interpolate(float t) const;
|
||||
|
||||
// Helper to apply value to target
|
||||
void applyValue(UIDrawable* target, const AnimationValue& value);
|
||||
void applyValue(UIEntity* entity, const AnimationValue& value);
|
||||
|
||||
// Trigger callback when animation completes
|
||||
void triggerCallback();
|
||||
};
|
||||
|
||||
// Easing functions library
|
||||
|
|
@ -134,13 +164,12 @@ public:
|
|||
// Update all animations
|
||||
void update(float deltaTime);
|
||||
|
||||
// Remove completed animations
|
||||
void cleanup();
|
||||
|
||||
// Clear all animations
|
||||
void clear();
|
||||
// Clear all animations (optionally completing them first)
|
||||
void clear(bool completeAnimations = false);
|
||||
|
||||
private:
|
||||
AnimationManager() = default;
|
||||
std::vector<std::shared_ptr<Animation>> activeAnimations;
|
||||
std::vector<std::shared_ptr<Animation>> pendingAnimations; // Animations to add after update
|
||||
bool isUpdating = false; // Flag to track if we're in update loop
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
#include "UITestScene.h"
|
||||
#include "Resources.h"
|
||||
#include "Animation.h"
|
||||
#include "Timer.h"
|
||||
#include <cmath>
|
||||
|
||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
||||
|
|
@ -16,7 +17,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
|||
{
|
||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||
Resources::game = this;
|
||||
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
|
||||
window_title = "McRogueFace Engine";
|
||||
|
||||
// Initialize rendering based on headless mode
|
||||
if (headless) {
|
||||
|
|
@ -91,6 +92,9 @@ void GameEngine::cleanup()
|
|||
if (cleaned_up) return;
|
||||
cleaned_up = true;
|
||||
|
||||
// Clear all animations first (RAII handles invalidation)
|
||||
AnimationManager::getInstance().clear();
|
||||
|
||||
// Clear Python references before destroying C++ objects
|
||||
// Clear all timers (they hold Python callables)
|
||||
timers.clear();
|
||||
|
|
@ -182,7 +186,7 @@ void GameEngine::setWindowScale(float multiplier)
|
|||
|
||||
void GameEngine::run()
|
||||
{
|
||||
std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
//std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
float fps = 0.0;
|
||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
||||
clock.restart();
|
||||
|
|
@ -259,7 +263,7 @@ void GameEngine::run()
|
|||
int tenth_fps = (metrics.fps * 10) % 10;
|
||||
|
||||
if (!headless && window) {
|
||||
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||
window->setTitle(window_title);
|
||||
}
|
||||
|
||||
// In windowed mode, check if window was closed
|
||||
|
|
@ -272,7 +276,7 @@ void GameEngine::run()
|
|||
cleanup();
|
||||
}
|
||||
|
||||
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
|
||||
std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
|
||||
{
|
||||
auto it = timers.find(name);
|
||||
if (it != timers.end()) {
|
||||
|
|
@ -290,7 +294,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
|||
{
|
||||
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
|
||||
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
|
||||
timers[name] = std::make_shared<PyTimerCallable>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
||||
timers[name] = std::make_shared<Timer>(Py_None, 1000, runtime.getElapsedTime().asMilliseconds());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -299,7 +303,7 @@ void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
|||
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
|
||||
return;
|
||||
}
|
||||
timers[name] = std::make_shared<PyTimerCallable>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
||||
timers[name] = std::make_shared<Timer>(target, interval, runtime.getElapsedTime().asMilliseconds());
|
||||
}
|
||||
|
||||
void GameEngine::testTimers()
|
||||
|
|
@ -310,7 +314,8 @@ void GameEngine::testTimers()
|
|||
{
|
||||
it->second->test(now);
|
||||
|
||||
if (it->second->isNone())
|
||||
// Remove timers that have been cancelled or are one-shot and fired
|
||||
if (!it->second->getCallback() || it->second->getCallback() == Py_None)
|
||||
{
|
||||
it = timers.erase(it);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,8 +58,7 @@ private:
|
|||
|
||||
public:
|
||||
sf::Clock runtime;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
std::map<std::string, std::shared_ptr<Timer>> timers;
|
||||
std::string scene;
|
||||
|
||||
// Profiling metrics
|
||||
|
|
@ -116,7 +115,7 @@ public:
|
|||
float getFrameTime() { return frameTime; }
|
||||
sf::View getView() { return visible; }
|
||||
void manageTimer(std::string, PyObject*, int);
|
||||
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
|
||||
std::shared_ptr<Timer> getTimer(const std::string& name);
|
||||
void setWindowScale(float);
|
||||
bool isHeadless() const { return headless; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
|
|
|||
|
|
@ -267,6 +267,14 @@ PyObject* PyInit_mcrfpy()
|
|||
PySceneType.tp_methods = PySceneClass::methods;
|
||||
PySceneType.tp_getset = PySceneClass::getsetters;
|
||||
|
||||
// Set up weakref support for all types that need it
|
||||
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
|
||||
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);
|
||||
PyUICaptionType.tp_weaklistoffset = offsetof(PyUICaptionObject, weakreflist);
|
||||
PyUISpriteType.tp_weaklistoffset = offsetof(PyUISpriteObject, weakreflist);
|
||||
PyUIGridType.tp_weaklistoffset = offsetof(PyUIGridObject, weakreflist);
|
||||
PyUIEntityType.tp_weaklistoffset = offsetof(PyUIEntityObject, weakreflist);
|
||||
|
||||
int i = 0;
|
||||
auto t = pytypes[i];
|
||||
while (t != nullptr)
|
||||
|
|
|
|||
|
|
@ -18,19 +18,31 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
|
|||
}
|
||||
|
||||
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
|
||||
|
||||
const char* property_name;
|
||||
PyObject* target_value;
|
||||
float duration;
|
||||
const char* easing_name = "linear";
|
||||
int delta = 0;
|
||||
PyObject* callback = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate callback is callable if provided
|
||||
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert None to nullptr for C++
|
||||
if (callback == Py_None) {
|
||||
callback = nullptr;
|
||||
}
|
||||
|
||||
// Convert Python target value to AnimationValue
|
||||
AnimationValue animValue;
|
||||
|
||||
|
|
@ -90,7 +102,7 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
|||
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
||||
|
||||
// Create the Animation
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -126,50 +138,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Get the UIDrawable from the Python object
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
// Check type by comparing type names
|
||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||
|
||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||
drawable = frame->data.get();
|
||||
if (frame->data) {
|
||||
self->data->start(frame->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||
drawable = caption->data.get();
|
||||
if (caption->data) {
|
||||
self->data->start(caption->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||
drawable = sprite->data.get();
|
||||
if (sprite->data) {
|
||||
self->data->start(sprite->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
drawable = grid->data.get();
|
||||
if (grid->data) {
|
||||
self->data->start(grid->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||
// Start the animation directly on the entity
|
||||
self->data->startEntity(entity->data.get());
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
if (entity->data) {
|
||||
self->data->startEntity(entity->data);
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
}
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
self->data->start(drawable);
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +226,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
|
|||
}, value);
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) {
|
||||
if (self->data) {
|
||||
self->data->complete();
|
||||
}
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) {
|
||||
if (self->data && self->data->hasValidTarget()) {
|
||||
Py_RETURN_TRUE;
|
||||
}
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
PyGetSetDef PyAnimation::getsetters[] = {
|
||||
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
||||
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
||||
|
|
@ -225,10 +251,23 @@ PyGetSetDef PyAnimation::getsetters[] = {
|
|||
|
||||
PyMethodDef PyAnimation::methods[] = {
|
||||
{"start", (PyCFunction)start, METH_VARARGS,
|
||||
"Start the animation on a target UIDrawable"},
|
||||
"start(target) -> None\n\n"
|
||||
"Start the animation on a target UI element.\n\n"
|
||||
"Args:\n"
|
||||
" target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)\n\n"
|
||||
"Note:\n"
|
||||
" The animation will automatically stop if the target is destroyed."},
|
||||
{"update", (PyCFunction)update, METH_VARARGS,
|
||||
"Update the animation by deltaTime (returns True if still running)"},
|
||||
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
||||
"Get the current interpolated value"},
|
||||
{"complete", (PyCFunction)complete, METH_NOARGS,
|
||||
"complete() -> None\n\n"
|
||||
"Complete the animation immediately by jumping to the final value."},
|
||||
{"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS,
|
||||
"hasValidTarget() -> bool\n\n"
|
||||
"Check if the animation still has a valid target.\n\n"
|
||||
"Returns:\n"
|
||||
" True if the target still exists, False if it was destroyed."},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -28,6 +28,8 @@ public:
|
|||
static PyObject* start(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* update(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* complete(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* has_valid_target(PyAnimationObject* self, PyObject* args);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
|
|
|
|||
|
|
@ -1,410 +0,0 @@
|
|||
#pragma once
|
||||
#include "Python.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyColor.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <string>
|
||||
|
||||
// Unified argument parsing helpers for Python API consistency
|
||||
namespace PyArgHelpers {
|
||||
|
||||
// Position in pixels (float)
|
||||
struct PositionResult {
|
||||
float x, y;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Size in pixels (float)
|
||||
struct SizeResult {
|
||||
float w, h;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Grid position in tiles (float - for animation)
|
||||
struct GridPositionResult {
|
||||
float grid_x, grid_y;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Grid size in tiles (int - can't have fractional tiles)
|
||||
struct GridSizeResult {
|
||||
int grid_w, grid_h;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Color parsing
|
||||
struct ColorResult {
|
||||
sf::Color color;
|
||||
bool valid;
|
||||
const char* error;
|
||||
};
|
||||
|
||||
// Helper to check if a keyword conflicts with positional args
|
||||
static bool hasConflict(PyObject* kwds, const char* key, bool has_positional) {
|
||||
if (!kwds || !has_positional) return false;
|
||||
PyObject* value = PyDict_GetItemString(kwds, key);
|
||||
return value != nullptr;
|
||||
}
|
||||
|
||||
// Parse position with conflict detection
|
||||
static PositionResult parsePosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
PositionResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument first
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
// Is it a tuple/Vector?
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
// Extract from tuple
|
||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
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.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(first, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
// It's a Vector object
|
||||
PyVectorObject* vec = (PyVectorObject*)first;
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "pos", true) || hasConflict(kwds, "x", true) || hasConflict(kwds, "y", true)) {
|
||||
result.valid = false;
|
||||
result.error = "position specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
|
||||
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
|
||||
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
|
||||
|
||||
// Check for conflicts between pos and x/y
|
||||
if (pos_obj && (x_obj || y_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "pos and x/y cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (pos_obj) {
|
||||
// Parse pos keyword
|
||||
if (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 ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
result.x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
result.y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
result.valid = true;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
result.x = vec->data.x;
|
||||
result.y = vec->data.y;
|
||||
result.valid = true;
|
||||
}
|
||||
} else if (x_obj && y_obj) {
|
||||
// Parse x, y keywords
|
||||
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.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse size with conflict detection
|
||||
static SizeResult parseSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
SizeResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "size", true) || hasConflict(kwds, "w", true) || hasConflict(kwds, "h", true)) {
|
||||
result.valid = false;
|
||||
result.error = "size specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* size_obj = PyDict_GetItemString(kwds, "size");
|
||||
PyObject* w_obj = PyDict_GetItemString(kwds, "w");
|
||||
PyObject* h_obj = PyDict_GetItemString(kwds, "h");
|
||||
|
||||
// Check for conflicts between size and w/h
|
||||
if (size_obj && (w_obj || h_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "size and w/h cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (size_obj) {
|
||||
// Parse size keyword
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
result.w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
result.h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
} else if (w_obj && h_obj) {
|
||||
// Parse w, h keywords
|
||||
if ((PyFloat_Check(w_obj) || PyLong_Check(w_obj)) &&
|
||||
(PyFloat_Check(h_obj) || PyLong_Check(h_obj))) {
|
||||
result.w = PyFloat_Check(w_obj) ? PyFloat_AsDouble(w_obj) : PyLong_AsLong(w_obj);
|
||||
result.h = PyFloat_Check(h_obj) ? PyFloat_AsDouble(h_obj) : PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse grid position (float for smooth animation)
|
||||
static GridPositionResult parseGridPosition(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
GridPositionResult result = {0.0f, 0.0f, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
|
||||
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
|
||||
result.grid_x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
|
||||
result.grid_y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "grid_pos", true) || hasConflict(kwds, "grid_x", true) || hasConflict(kwds, "grid_y", true)) {
|
||||
result.valid = false;
|
||||
result.error = "grid position specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* grid_pos_obj = PyDict_GetItemString(kwds, "grid_pos");
|
||||
PyObject* grid_x_obj = PyDict_GetItemString(kwds, "grid_x");
|
||||
PyObject* grid_y_obj = PyDict_GetItemString(kwds, "grid_y");
|
||||
|
||||
// Check for conflicts between grid_pos and grid_x/grid_y
|
||||
if (grid_pos_obj && (grid_x_obj || grid_y_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "grid_pos and grid_x/grid_y cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (grid_pos_obj) {
|
||||
// Parse grid_pos keyword
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
result.grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
result.grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
} else if (grid_x_obj && grid_y_obj) {
|
||||
// Parse grid_x, grid_y keywords
|
||||
if ((PyFloat_Check(grid_x_obj) || PyLong_Check(grid_x_obj)) &&
|
||||
(PyFloat_Check(grid_y_obj) || PyLong_Check(grid_y_obj))) {
|
||||
result.grid_x = PyFloat_Check(grid_x_obj) ? PyFloat_AsDouble(grid_x_obj) : PyLong_AsLong(grid_x_obj);
|
||||
result.grid_y = PyFloat_Check(grid_y_obj) ? PyFloat_AsDouble(grid_y_obj) : PyLong_AsLong(grid_y_obj);
|
||||
result.valid = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse grid size (int - no fractional tiles)
|
||||
static GridSizeResult parseGridSize(PyObject* args, PyObject* kwds, int* next_arg = nullptr) {
|
||||
GridSizeResult result = {0, 0, false, nullptr};
|
||||
int start_idx = next_arg ? *next_arg : 0;
|
||||
bool has_positional = false;
|
||||
|
||||
// Check for positional tuple argument
|
||||
if (args && PyTuple_Size(args) > start_idx) {
|
||||
PyObject* first = PyTuple_GetItem(args, start_idx);
|
||||
|
||||
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
|
||||
PyObject* w_obj = PyTuple_GetItem(first, 0);
|
||||
PyObject* h_obj = PyTuple_GetItem(first, 1);
|
||||
|
||||
if (PyLong_Check(w_obj) && PyLong_Check(h_obj)) {
|
||||
result.grid_w = PyLong_AsLong(w_obj);
|
||||
result.grid_h = PyLong_AsLong(h_obj);
|
||||
result.valid = true;
|
||||
has_positional = true;
|
||||
if (next_arg) (*next_arg)++;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keyword conflicts
|
||||
if (has_positional) {
|
||||
if (hasConflict(kwds, "grid_size", true) || hasConflict(kwds, "grid_w", true) || hasConflict(kwds, "grid_h", true)) {
|
||||
result.valid = false;
|
||||
result.error = "grid size specified both positionally and by keyword";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// If no positional, try keywords
|
||||
if (!has_positional && kwds) {
|
||||
PyObject* grid_size_obj = PyDict_GetItemString(kwds, "grid_size");
|
||||
PyObject* grid_w_obj = PyDict_GetItemString(kwds, "grid_w");
|
||||
PyObject* grid_h_obj = PyDict_GetItemString(kwds, "grid_h");
|
||||
|
||||
// Check for conflicts between grid_size and grid_w/grid_h
|
||||
if (grid_size_obj && (grid_w_obj || grid_h_obj)) {
|
||||
result.valid = false;
|
||||
result.error = "grid_size and grid_w/grid_h cannot both be specified";
|
||||
return result;
|
||||
}
|
||||
|
||||
if (grid_size_obj) {
|
||||
// Parse grid_size keyword
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(grid_size_obj, 1);
|
||||
|
||||
if (PyLong_Check(w_val) && PyLong_Check(h_val)) {
|
||||
result.grid_w = PyLong_AsLong(w_val);
|
||||
result.grid_h = PyLong_AsLong(h_val);
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if (grid_w_obj && grid_h_obj) {
|
||||
// Parse grid_w, grid_h keywords
|
||||
if (PyLong_Check(grid_w_obj) && PyLong_Check(grid_h_obj)) {
|
||||
result.grid_w = PyLong_AsLong(grid_w_obj);
|
||||
result.grid_h = PyLong_AsLong(grid_h_obj);
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
result.error = "grid size must be specified with integers";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse color using existing PyColor infrastructure
|
||||
static ColorResult parseColor(PyObject* obj, const char* param_name = nullptr) {
|
||||
ColorResult result = {sf::Color::White, false, nullptr};
|
||||
|
||||
if (!obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Use existing PyColor::from_arg which handles tuple/Color conversion
|
||||
auto py_color = PyColor::from_arg(obj);
|
||||
if (py_color) {
|
||||
result.color = py_color->data;
|
||||
result.valid = true;
|
||||
} else {
|
||||
result.valid = false;
|
||||
std::string error_msg = param_name
|
||||
? std::string(param_name) + " must be a color tuple (r,g,b) or (r,g,b,a)"
|
||||
: "Invalid color format - expected tuple (r,g,b) or (r,g,b,a)";
|
||||
result.error = error_msg.c_str();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper to validate a texture object
|
||||
static bool isValidTexture(PyObject* obj) {
|
||||
if (!obj) return false;
|
||||
PyObject* texture_type = PyObject_GetAttrString(PyImport_ImportModule("mcrfpy"), "Texture");
|
||||
bool is_texture = PyObject_IsInstance(obj, texture_type);
|
||||
Py_DECREF(texture_type);
|
||||
return is_texture;
|
||||
}
|
||||
|
||||
// Helper to validate a click handler
|
||||
static bool isValidClickHandler(PyObject* obj) {
|
||||
return obj && PyCallable_Check(obj);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,21 @@ PyCallable::PyCallable(PyObject* _target)
|
|||
target = Py_XNewRef(_target);
|
||||
}
|
||||
|
||||
PyCallable::PyCallable(const PyCallable& other)
|
||||
{
|
||||
target = Py_XNewRef(other.target);
|
||||
}
|
||||
|
||||
PyCallable& PyCallable::operator=(const PyCallable& other)
|
||||
{
|
||||
if (this != &other) {
|
||||
PyObject* old_target = target;
|
||||
target = Py_XNewRef(other.target);
|
||||
Py_XDECREF(old_target);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
PyCallable::~PyCallable()
|
||||
{
|
||||
if (target)
|
||||
|
|
@ -21,103 +36,6 @@ bool PyCallable::isNone() const
|
|||
return (target == Py_None || target == NULL);
|
||||
}
|
||||
|
||||
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
|
||||
: PyCallable(_target), interval(_interval), last_ran(now),
|
||||
paused(false), pause_start_time(0), total_paused_time(0)
|
||||
{}
|
||||
|
||||
PyTimerCallable::PyTimerCallable()
|
||||
: PyCallable(Py_None), interval(0), last_ran(0),
|
||||
paused(false), pause_start_time(0), total_paused_time(0)
|
||||
{}
|
||||
|
||||
bool PyTimerCallable::hasElapsed(int now)
|
||||
{
|
||||
if (paused) return false;
|
||||
return now >= last_ran + interval;
|
||||
}
|
||||
|
||||
void PyTimerCallable::call(int now)
|
||||
{
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyCallable::call(args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
std::cout << PyUnicode_AsUTF8(PyObject_Repr(retval)) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
bool PyTimerCallable::test(int now)
|
||||
{
|
||||
if(hasElapsed(now))
|
||||
{
|
||||
call(now);
|
||||
last_ran = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void PyTimerCallable::pause(int current_time)
|
||||
{
|
||||
if (!paused) {
|
||||
paused = true;
|
||||
pause_start_time = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
void PyTimerCallable::resume(int current_time)
|
||||
{
|
||||
if (paused) {
|
||||
paused = false;
|
||||
int paused_duration = current_time - pause_start_time;
|
||||
total_paused_time += paused_duration;
|
||||
// Adjust last_ran to account for the pause
|
||||
last_ran += paused_duration;
|
||||
}
|
||||
}
|
||||
|
||||
void PyTimerCallable::restart(int current_time)
|
||||
{
|
||||
last_ran = current_time;
|
||||
paused = false;
|
||||
pause_start_time = 0;
|
||||
total_paused_time = 0;
|
||||
}
|
||||
|
||||
void PyTimerCallable::cancel()
|
||||
{
|
||||
// Cancel by setting target to None
|
||||
if (target && target != Py_None) {
|
||||
Py_DECREF(target);
|
||||
}
|
||||
target = Py_None;
|
||||
Py_INCREF(Py_None);
|
||||
}
|
||||
|
||||
int PyTimerCallable::getRemaining(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
// When paused, calculate time remaining from when it was paused
|
||||
int elapsed_when_paused = pause_start_time - last_ran;
|
||||
return interval - elapsed_when_paused;
|
||||
}
|
||||
int elapsed = current_time - last_ran;
|
||||
return interval - elapsed;
|
||||
}
|
||||
|
||||
void PyTimerCallable::setCallback(PyObject* new_callback)
|
||||
{
|
||||
if (target && target != Py_None) {
|
||||
Py_DECREF(target);
|
||||
}
|
||||
target = Py_XNewRef(new_callback);
|
||||
}
|
||||
|
||||
PyClickCallable::PyClickCallable(PyObject* _target)
|
||||
: PyCallable(_target)
|
||||
|
|
|
|||
|
|
@ -6,45 +6,15 @@ class PyCallable
|
|||
{
|
||||
protected:
|
||||
PyObject* target;
|
||||
|
||||
public:
|
||||
PyCallable(PyObject*);
|
||||
PyCallable(const PyCallable& other);
|
||||
PyCallable& operator=(const PyCallable& other);
|
||||
~PyCallable();
|
||||
PyObject* call(PyObject*, PyObject*);
|
||||
public:
|
||||
bool isNone() const;
|
||||
};
|
||||
|
||||
class PyTimerCallable: public PyCallable
|
||||
{
|
||||
private:
|
||||
int interval;
|
||||
int last_ran;
|
||||
void call(int);
|
||||
|
||||
// Pause/resume support
|
||||
bool paused;
|
||||
int pause_start_time;
|
||||
int total_paused_time;
|
||||
|
||||
public:
|
||||
bool hasElapsed(int);
|
||||
bool test(int);
|
||||
PyTimerCallable(PyObject*, int, int);
|
||||
PyTimerCallable();
|
||||
|
||||
// Timer control methods
|
||||
void pause(int current_time);
|
||||
void resume(int current_time);
|
||||
void restart(int current_time);
|
||||
void cancel();
|
||||
|
||||
// Timer state queries
|
||||
bool isPaused() const { return paused; }
|
||||
bool isActive() const { return !isNone() && !paused; }
|
||||
int getInterval() const { return interval; }
|
||||
void setInterval(int new_interval) { interval = new_interval; }
|
||||
int getRemaining(int current_time) const;
|
||||
PyObject* getCallback() { return target; }
|
||||
void setCallback(PyObject* new_callback);
|
||||
PyObject* borrow() const { return target; }
|
||||
};
|
||||
|
||||
class PyClickCallable: public PyCallable
|
||||
|
|
@ -54,6 +24,11 @@ public:
|
|||
PyObject* borrow();
|
||||
PyClickCallable(PyObject*);
|
||||
PyClickCallable();
|
||||
PyClickCallable(const PyClickCallable& other) : PyCallable(other) {}
|
||||
PyClickCallable& operator=(const PyClickCallable& other) {
|
||||
PyCallable::operator=(other);
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class PyKeyCallable: public PyCallable
|
||||
|
|
|
|||
|
|
@ -31,13 +31,18 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
|||
// Convert window coordinates to game coordinates using the viewport
|
||||
auto mousepos = game->windowToGameCoords(sf::Vector2f(unscaledmousepos));
|
||||
|
||||
// Create a sorted copy by z-index (highest first)
|
||||
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
||||
std::sort(sorted_elements.begin(), sorted_elements.end(),
|
||||
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
|
||||
// Only sort if z_index values have changed
|
||||
if (ui_elements_need_sort) {
|
||||
// Sort in ascending order (same as render)
|
||||
std::sort(ui_elements->begin(), ui_elements->end(),
|
||||
[](const auto& a, const auto& b) { return a->z_index < b->z_index; });
|
||||
ui_elements_need_sort = false;
|
||||
}
|
||||
|
||||
// Check elements in z-order (top to bottom)
|
||||
for (const auto& element : sorted_elements) {
|
||||
// Check elements in reverse z-order (highest z_index first, top to bottom)
|
||||
// Use reverse iterators to go from end to beginning
|
||||
for (auto it = ui_elements->rbegin(); it != ui_elements->rend(); ++it) {
|
||||
const auto& element = *it;
|
||||
if (!element->visible) continue;
|
||||
|
||||
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
#include "PyTimer.h"
|
||||
#include "PyCallable.h"
|
||||
#include "Timer.h"
|
||||
#include "GameEngine.h"
|
||||
#include "Resources.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <sstream>
|
||||
|
||||
PyObject* PyTimer::repr(PyObject* self) {
|
||||
|
|
@ -11,7 +12,22 @@ PyObject* PyTimer::repr(PyObject* self) {
|
|||
|
||||
if (timer->data) {
|
||||
oss << "interval=" << timer->data->getInterval() << "ms ";
|
||||
oss << (timer->data->isPaused() ? "paused" : "active");
|
||||
if (timer->data->isOnce()) {
|
||||
oss << "once=True ";
|
||||
}
|
||||
if (timer->data->isPaused()) {
|
||||
oss << "paused";
|
||||
// Get current time to show remaining
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
|
||||
} else if (timer->data->isActive()) {
|
||||
oss << "active";
|
||||
} else {
|
||||
oss << "cancelled";
|
||||
}
|
||||
} else {
|
||||
oss << "uninitialized";
|
||||
}
|
||||
|
|
@ -25,18 +41,20 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
|||
if (self) {
|
||||
new(&self->name) std::string(); // Placement new for std::string
|
||||
self->data = nullptr;
|
||||
self->weakreflist = nullptr; // Initialize weakref list
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* kwlist[] = {"name", "callback", "interval", NULL};
|
||||
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL};
|
||||
const char* name = nullptr;
|
||||
PyObject* callback = nullptr;
|
||||
int interval = 0;
|
||||
int once = 0; // Use int for bool parameter
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", const_cast<char**>(kwlist),
|
||||
&name, &callback, &interval)) {
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
|
||||
&name, &callback, &interval, &once)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -58,8 +76,18 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
|||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
// Create the timer callable
|
||||
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time);
|
||||
// Create the timer
|
||||
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once);
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
// Register with game engine
|
||||
if (Resources::game) {
|
||||
|
|
@ -70,6 +98,11 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
|
||||
void PyTimer::dealloc(PyTimerObject* self) {
|
||||
// Clear weakrefs first
|
||||
if (self->weakreflist != nullptr) {
|
||||
PyObject_ClearWeakRefs((PyObject*)self);
|
||||
}
|
||||
|
||||
// Remove from game engine if still registered
|
||||
if (Resources::game && !self->name.empty()) {
|
||||
auto it = Resources::game->timers.find(self->name);
|
||||
|
|
@ -244,7 +277,37 @@ int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_once(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isOnce());
|
||||
}
|
||||
|
||||
int PyTimer::set_once(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyBool_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "once must be a boolean");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setOnce(PyObject_IsTrue(value));
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_name(PyTimerObject* self, void* closure) {
|
||||
return PyUnicode_FromString(self->name.c_str());
|
||||
}
|
||||
|
||||
PyGetSetDef PyTimer::getsetters[] = {
|
||||
{"name", (getter)PyTimer::get_name, NULL,
|
||||
"Timer name (read-only)", NULL},
|
||||
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
|
||||
"Timer interval in milliseconds", NULL},
|
||||
{"remaining", (getter)PyTimer::get_remaining, NULL,
|
||||
|
|
@ -255,17 +318,27 @@ PyGetSetDef PyTimer::getsetters[] = {
|
|||
"Whether the timer is active and not paused", NULL},
|
||||
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
|
||||
"The callback function to be called", NULL},
|
||||
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
|
||||
"Whether the timer stops after firing once", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyTimer::methods[] = {
|
||||
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
|
||||
"Pause the timer"},
|
||||
"pause() -> None\n\n"
|
||||
"Pause the timer, preserving the time remaining until next trigger.\n"
|
||||
"The timer can be resumed later with resume()."},
|
||||
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
|
||||
"Resume a paused timer"},
|
||||
"resume() -> None\n\n"
|
||||
"Resume a paused timer from where it left off.\n"
|
||||
"Has no effect if the timer is not paused."},
|
||||
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
|
||||
"Cancel the timer and remove it from the system"},
|
||||
"cancel() -> None\n\n"
|
||||
"Cancel the timer and remove it from the timer system.\n"
|
||||
"The timer will no longer fire and cannot be restarted."},
|
||||
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
|
||||
"Restart the timer from the current time"},
|
||||
"restart() -> None\n\n"
|
||||
"Restart the timer from the beginning.\n"
|
||||
"Resets the timer to fire after a full interval from now."},
|
||||
{NULL}
|
||||
};
|
||||
|
|
@ -4,12 +4,13 @@
|
|||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class PyTimerCallable;
|
||||
class Timer;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<PyTimerCallable> data;
|
||||
std::shared_ptr<Timer> data;
|
||||
std::string name;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyTimerObject;
|
||||
|
||||
class PyTimer
|
||||
|
|
@ -28,6 +29,7 @@ public:
|
|||
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
|
||||
// Timer property getters
|
||||
static PyObject* get_name(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_interval(PyTimerObject* self, void* closure);
|
||||
static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_remaining(PyTimerObject* self, void* closure);
|
||||
|
|
@ -35,6 +37,8 @@ public:
|
|||
static PyObject* get_active(PyTimerObject* self, void* closure);
|
||||
static PyObject* get_callback(PyTimerObject* self, void* closure);
|
||||
static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_once(PyTimerObject* self, void* closure);
|
||||
static int set_once(PyTimerObject* self, PyObject* value, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
|
|
@ -49,7 +53,35 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)PyTimer::dealloc,
|
||||
.tp_repr = PyTimer::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"),
|
||||
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n"
|
||||
"Create a timer that calls a function at regular intervals.\n\n"
|
||||
"Args:\n"
|
||||
" name (str): Unique identifier for the timer\n"
|
||||
" callback (callable): Function to call - receives (timer, runtime) args\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" once (bool): If True, timer stops after first call. Default: False\n\n"
|
||||
"Attributes:\n"
|
||||
" interval (int): Time between calls in milliseconds\n"
|
||||
" remaining (int): Time until next call in milliseconds (read-only)\n"
|
||||
" paused (bool): Whether timer is paused (read-only)\n"
|
||||
" active (bool): Whether timer is active and not paused (read-only)\n"
|
||||
" callback (callable): The callback function\n"
|
||||
" once (bool): Whether timer stops after firing once\n\n"
|
||||
"Methods:\n"
|
||||
" pause(): Pause the timer, preserving time remaining\n"
|
||||
" resume(): Resume a paused timer\n"
|
||||
" cancel(): Stop and remove the timer\n"
|
||||
" restart(): Reset timer to start from beginning\n\n"
|
||||
"Example:\n"
|
||||
" def on_timer(timer, runtime):\n"
|
||||
" print(f'Timer {timer} fired at {runtime}ms')\n"
|
||||
" if runtime > 5000:\n"
|
||||
" timer.cancel()\n"
|
||||
" \n"
|
||||
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
|
||||
" timer.pause() # Pause timer\n"
|
||||
" timer.resume() # Resume timer\n"
|
||||
" timer.once = True # Make it one-shot"),
|
||||
.tp_methods = PyTimer::methods,
|
||||
.tp_getset = PyTimer::getsetters,
|
||||
.tp_init = (initproc)PyTimer::init,
|
||||
|
|
|
|||
85
src/PythonObjectCache.cpp
Normal file
85
src/PythonObjectCache.cpp
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#include "PythonObjectCache.h"
|
||||
#include <iostream>
|
||||
|
||||
PythonObjectCache* PythonObjectCache::instance = nullptr;
|
||||
|
||||
PythonObjectCache& PythonObjectCache::getInstance() {
|
||||
if (!instance) {
|
||||
instance = new PythonObjectCache();
|
||||
}
|
||||
return *instance;
|
||||
}
|
||||
|
||||
PythonObjectCache::~PythonObjectCache() {
|
||||
clear();
|
||||
}
|
||||
|
||||
uint64_t PythonObjectCache::assignSerial() {
|
||||
return next_serial.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PythonObjectCache::registerObject(uint64_t serial, PyObject* weakref) {
|
||||
if (!weakref || serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
// Clean up any existing entry
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
}
|
||||
|
||||
// Store the new weak reference
|
||||
Py_INCREF(weakref);
|
||||
cache[serial] = weakref;
|
||||
}
|
||||
|
||||
PyObject* PythonObjectCache::lookup(uint64_t serial) {
|
||||
if (serial == 0) return nullptr;
|
||||
|
||||
// No mutex needed for read - GIL protects PyWeakref_GetObject
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (obj && obj != Py_None) {
|
||||
Py_INCREF(obj);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void PythonObjectCache::remove(uint64_t serial) {
|
||||
if (serial == 0) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
auto it = cache.find(serial);
|
||||
if (it != cache.end()) {
|
||||
Py_DECREF(it->second);
|
||||
cache.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::cleanup() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
auto it = cache.begin();
|
||||
while (it != cache.end()) {
|
||||
PyObject* obj = PyWeakref_GetObject(it->second);
|
||||
if (!obj || obj == Py_None) {
|
||||
Py_DECREF(it->second);
|
||||
it = cache.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PythonObjectCache::clear() {
|
||||
std::lock_guard<std::mutex> lock(serial_mutex);
|
||||
|
||||
for (auto& pair : cache) {
|
||||
Py_DECREF(pair.second);
|
||||
}
|
||||
cache.clear();
|
||||
}
|
||||
40
src/PythonObjectCache.h
Normal file
40
src/PythonObjectCache.h
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <Python.h>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
class PythonObjectCache {
|
||||
private:
|
||||
static PythonObjectCache* instance;
|
||||
std::mutex serial_mutex;
|
||||
std::atomic<uint64_t> next_serial{1};
|
||||
std::unordered_map<uint64_t, PyObject*> cache;
|
||||
|
||||
PythonObjectCache() = default;
|
||||
~PythonObjectCache();
|
||||
|
||||
public:
|
||||
static PythonObjectCache& getInstance();
|
||||
|
||||
// Assign a new serial number
|
||||
uint64_t assignSerial();
|
||||
|
||||
// Register a Python object with a serial number
|
||||
void registerObject(uint64_t serial, PyObject* weakref);
|
||||
|
||||
// Lookup a Python object by serial number
|
||||
// Returns new reference or nullptr
|
||||
PyObject* lookup(uint64_t serial);
|
||||
|
||||
// Remove an entry from the cache
|
||||
void remove(uint64_t serial);
|
||||
|
||||
// Clean up dead weak references
|
||||
void cleanup();
|
||||
|
||||
// Clear entire cache (for module cleanup)
|
||||
void clear();
|
||||
};
|
||||
127
src/Timer.cpp
127
src/Timer.cpp
|
|
@ -1,31 +1,140 @@
|
|||
#include "Timer.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include "PyCallable.h"
|
||||
|
||||
Timer::Timer(PyObject* _target, int _interval, int now)
|
||||
: target(_target), interval(_interval), last_ran(now)
|
||||
Timer::Timer(PyObject* _target, int _interval, int now, bool _once)
|
||||
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(_once)
|
||||
{}
|
||||
|
||||
Timer::Timer()
|
||||
: target(Py_None), interval(0), last_ran(0)
|
||||
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
|
||||
paused(false), pause_start_time(0), total_paused_time(0), once(false)
|
||||
{}
|
||||
|
||||
Timer::~Timer() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
bool Timer::hasElapsed(int now) const
|
||||
{
|
||||
if (paused) return false;
|
||||
return now >= last_ran + interval;
|
||||
}
|
||||
|
||||
bool Timer::test(int now)
|
||||
{
|
||||
if (!target || target == Py_None) return false;
|
||||
if (now > last_ran + interval)
|
||||
if (!callback || callback->isNone()) return false;
|
||||
|
||||
if (hasElapsed(now))
|
||||
{
|
||||
last_ran = now;
|
||||
PyObject* args = Py_BuildValue("(i)", now);
|
||||
PyObject* retval = PyObject_Call(target, args, NULL);
|
||||
|
||||
// Get the PyTimer wrapper from cache to pass to callback
|
||||
PyObject* timer_obj = nullptr;
|
||||
if (serial_number != 0) {
|
||||
timer_obj = PythonObjectCache::getInstance().lookup(serial_number);
|
||||
}
|
||||
|
||||
// Build args: (timer, runtime) or just (runtime) if no wrapper found
|
||||
PyObject* args;
|
||||
if (timer_obj) {
|
||||
args = Py_BuildValue("(Oi)", timer_obj, now);
|
||||
} else {
|
||||
// Fallback to old behavior if no wrapper found
|
||||
args = Py_BuildValue("(i)", now);
|
||||
}
|
||||
|
||||
PyObject* retval = callback->call(args, NULL);
|
||||
Py_DECREF(args);
|
||||
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "timer has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
std::cout << "Timer callback has raised an exception. It's going to STDERR and being dropped:" << std::endl;
|
||||
PyErr_Print();
|
||||
PyErr_Clear();
|
||||
} else if (retval != Py_None)
|
||||
{
|
||||
std::cout << "timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
Py_DECREF(retval);
|
||||
}
|
||||
|
||||
// Handle one-shot timers
|
||||
if (once) {
|
||||
cancel();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Timer::pause(int current_time)
|
||||
{
|
||||
if (!paused) {
|
||||
paused = true;
|
||||
pause_start_time = current_time;
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::resume(int current_time)
|
||||
{
|
||||
if (paused) {
|
||||
paused = false;
|
||||
int paused_duration = current_time - pause_start_time;
|
||||
total_paused_time += paused_duration;
|
||||
// Adjust last_ran to account for the pause
|
||||
last_ran += paused_duration;
|
||||
}
|
||||
}
|
||||
|
||||
void Timer::restart(int current_time)
|
||||
{
|
||||
last_ran = current_time;
|
||||
paused = false;
|
||||
pause_start_time = 0;
|
||||
total_paused_time = 0;
|
||||
}
|
||||
|
||||
void Timer::cancel()
|
||||
{
|
||||
// Cancel by setting callback to None
|
||||
callback = std::make_shared<PyCallable>(Py_None);
|
||||
}
|
||||
|
||||
bool Timer::isActive() const
|
||||
{
|
||||
return callback && !callback->isNone() && !paused;
|
||||
}
|
||||
|
||||
int Timer::getRemaining(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
// When paused, calculate time remaining from when it was paused
|
||||
int elapsed_when_paused = pause_start_time - last_ran;
|
||||
return interval - elapsed_when_paused;
|
||||
}
|
||||
int elapsed = current_time - last_ran;
|
||||
return interval - elapsed;
|
||||
}
|
||||
|
||||
int Timer::getElapsed(int current_time) const
|
||||
{
|
||||
if (paused) {
|
||||
return pause_start_time - last_ran;
|
||||
}
|
||||
return current_time - last_ran;
|
||||
}
|
||||
|
||||
PyObject* Timer::getCallback()
|
||||
{
|
||||
if (!callback) return Py_None;
|
||||
return callback->borrow();
|
||||
}
|
||||
|
||||
void Timer::setCallback(PyObject* new_callback)
|
||||
{
|
||||
callback = std::make_shared<PyCallable>(new_callback);
|
||||
}
|
||||
47
src/Timer.h
47
src/Timer.h
|
|
@ -1,15 +1,54 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <memory>
|
||||
|
||||
class PyCallable;
|
||||
class GameEngine; // forward declare
|
||||
|
||||
class Timer
|
||||
{
|
||||
public:
|
||||
PyObject* target;
|
||||
private:
|
||||
std::shared_ptr<PyCallable> callback;
|
||||
int interval;
|
||||
int last_ran;
|
||||
|
||||
// Pause/resume support
|
||||
bool paused;
|
||||
int pause_start_time;
|
||||
int total_paused_time;
|
||||
|
||||
// One-shot timer support
|
||||
bool once;
|
||||
|
||||
public:
|
||||
uint64_t serial_number = 0; // For Python object cache
|
||||
|
||||
Timer(); // for map to build
|
||||
Timer(PyObject*, int, int);
|
||||
bool test(int);
|
||||
Timer(PyObject* target, int interval, int now, bool once = false);
|
||||
~Timer();
|
||||
|
||||
// Core timer functionality
|
||||
bool test(int now);
|
||||
bool hasElapsed(int now) const;
|
||||
|
||||
// Timer control methods
|
||||
void pause(int current_time);
|
||||
void resume(int current_time);
|
||||
void restart(int current_time);
|
||||
void cancel();
|
||||
|
||||
// Timer state queries
|
||||
bool isPaused() const { return paused; }
|
||||
bool isActive() const;
|
||||
int getInterval() const { return interval; }
|
||||
void setInterval(int new_interval) { interval = new_interval; }
|
||||
int getRemaining(int current_time) const;
|
||||
int getElapsed(int current_time) const;
|
||||
bool isOnce() const { return once; }
|
||||
void setOnce(bool value) { once = value; }
|
||||
|
||||
// Callback management
|
||||
PyObject* getCallback();
|
||||
void setCallback(PyObject* new_callback);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ class UIEntity;
|
|||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIEntity> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIEntityObject;
|
||||
|
||||
class UIFrame;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIFrame> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIFrameObject;
|
||||
|
||||
class UICaption;
|
||||
|
|
@ -19,18 +21,21 @@ typedef struct {
|
|||
PyObject_HEAD
|
||||
std::shared_ptr<UICaption> data;
|
||||
PyObject* font;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUICaptionObject;
|
||||
|
||||
class UIGrid;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UIGrid> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUIGridObject;
|
||||
|
||||
class UISprite;
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<UISprite> data;
|
||||
PyObject* weakreflist; // Weak reference support
|
||||
} PyUISpriteObject;
|
||||
|
||||
// Common Python method implementations for UIDrawable-derived classes
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -303,183 +303,135 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
{
|
||||
using namespace mcrfpydef;
|
||||
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, outline = 0.0f;
|
||||
char* text = nullptr;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* font = nullptr;
|
||||
const char* text = "";
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* outline_color = nullptr;
|
||||
float outline = 0.0f;
|
||||
float font_size = 16.0f;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got position from helpers (tuple format)
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"text", "font", "fill_color", "outline_color", "outline", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|zOOOfO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&text, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "font", "text", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "font_size", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOzOOffOifizff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &font, &text, // Positional
|
||||
&fill_color, &outline_color, &outline, &font_size, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (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 ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle font argument
|
||||
std::shared_ptr<PyFont> pyfont = nullptr;
|
||||
if (font && font != Py_None) {
|
||||
if (!PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
// First check if this is the old (text, x, y, ...) format
|
||||
PyObject* first_arg = args && PyTuple_Size(args) > 0 ? PyTuple_GetItem(args, 0) : nullptr;
|
||||
bool text_first = first_arg && PyUnicode_Check(first_arg);
|
||||
|
||||
if (text_first) {
|
||||
// Pattern: (text, x, y, ...)
|
||||
static const char* text_first_keywords[] = {
|
||||
"text", "x", "y", "font", "fill_color", "outline_color",
|
||||
"outline", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO",
|
||||
const_cast<char**>(text_first_keywords),
|
||||
&text, &x, &y, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
if (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 ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern: (x, y, text, ...)
|
||||
static const char* xy_keywords[] = {
|
||||
"x", "y", "text", "font", "fill_color", "outline_color",
|
||||
"outline", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO",
|
||||
const_cast<char**>(xy_keywords),
|
||||
&x, &y, &text, &font, &fill_color, &outline_color,
|
||||
&outline, &click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
if (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 ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto obj = (PyFontObject*)font;
|
||||
pyfont = obj->data;
|
||||
}
|
||||
|
||||
self->data->position = sf::Vector2f(x, y); // Set base class position
|
||||
self->data->text.setPosition(self->data->position); // Sync text position
|
||||
// check types for font, fill_color, outline_color
|
||||
|
||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
|
||||
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
|
||||
return -1;
|
||||
} else if (font != NULL && font != Py_None)
|
||||
{
|
||||
auto font_obj = (PyFontObject*)font;
|
||||
self->data->text.setFont(font_obj->data->font);
|
||||
self->font = font;
|
||||
Py_INCREF(font);
|
||||
} else
|
||||
{
|
||||
// Create the caption
|
||||
self->data = std::make_shared<UICaption>();
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
self->data->text.setPosition(self->data->position);
|
||||
self->data->text.setOutlineThickness(outline);
|
||||
|
||||
// Set the font
|
||||
if (pyfont) {
|
||||
self->data->text.setFont(pyfont->font);
|
||||
} else {
|
||||
// Use default font when None or not provided
|
||||
if (McRFPy_API::default_font) {
|
||||
self->data->text.setFont(McRFPy_API::default_font->font);
|
||||
// Store reference to default font
|
||||
PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font");
|
||||
if (default_font_obj) {
|
||||
self->font = default_font_obj;
|
||||
// Don't need to DECREF since we're storing it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle text - default to empty string if not provided
|
||||
if (text && text != NULL) {
|
||||
self->data->text.setString((std::string)text);
|
||||
} else {
|
||||
self->data->text.setString("");
|
||||
|
||||
// Set character size
|
||||
self->data->text.setCharacterSize(static_cast<unsigned int>(font_size));
|
||||
|
||||
// Set text
|
||||
if (text && strlen(text) > 0) {
|
||||
self->data->text.setString(std::string(text));
|
||||
}
|
||||
self->data->text.setOutlineThickness(outline);
|
||||
if (fill_color) {
|
||||
auto fc = PyColor::from_arg(fill_color);
|
||||
if (!fc) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->text.setFillColor(PyColor::fromPy(fc));
|
||||
//Py_DECREF(fc);
|
||||
self->data->text.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->text.setFillColor(sf::Color(0,0,0,255));
|
||||
self->data->text.setFillColor(sf::Color(255, 255, 255, 255)); // Default: white
|
||||
}
|
||||
|
||||
if (outline_color) {
|
||||
auto oc = PyColor::from_arg(outline_color);
|
||||
if (!oc) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be mcrfpy.Color or arguments to mcrfpy.Color.__init__");
|
||||
|
||||
// Handle outline_color
|
||||
if (outline_color && outline_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(outline_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->text.setOutlineColor(PyColor::fromPy(oc));
|
||||
//Py_DECREF(oc);
|
||||
self->data->text.setOutlineColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->text.setOutlineColor(sf::Color(128,128,128,255));
|
||||
self->data->text.setOutlineColor(sf::Color(0, 0, 0, 255)); // Default: black
|
||||
}
|
||||
|
||||
// Process click handler if provided
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
|
|
@ -487,10 +439,24 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// Property system implementation for animations
|
||||
bool UICaption::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUICaptionObject* obj = (PyUICaptionObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// TODO - reevaluate with PyFont usage; UICaption does not own the font
|
||||
// release reference to font object
|
||||
if (obj->font) Py_DECREF(obj->font);
|
||||
|
|
@ -64,27 +68,38 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Caption(text='', x=0, y=0, font=None, fill_color=None, outline_color=None, outline=0, click=None)\n\n"
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR("Caption(pos=None, font=None, text='', **kwargs)\n\n"
|
||||
"A text display UI element with customizable font and styling.\n\n"
|
||||
"Args:\n"
|
||||
" text (str): The text content to display. Default: ''\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" font (Font): Font object for text rendering. Default: engine default font\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" font (Font, optional): Font object for text rendering. Default: engine default font\n"
|
||||
" text (str, optional): The text content to display. Default: ''\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Text fill color. Default: (255, 255, 255, 255)\n"
|
||||
" outline_color (Color): Text outline color. Default: (0, 0, 0, 255)\n"
|
||||
" outline (float): Text outline thickness. Default: 0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" font_size (float): Font size in points. Default: 16\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" text (str): The displayed text content\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" font (Font): Font used for rendering\n"
|
||||
" font_size (float): Font size in points\n"
|
||||
" fill_color, outline_color (Color): Text appearance\n"
|
||||
" outline (float): Outline thickness\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on text and font"),
|
||||
.tp_methods = UICaption_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
@ -95,7 +110,11 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUICaptionObject* self = (PyUICaptionObject*)type->tp_alloc(type, 0);
|
||||
if (self) self->data = std::make_shared<UICaption>();
|
||||
if (self) {
|
||||
self->data = std::make_shared<UICaption>();
|
||||
self->font = nullptr;
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
|
||||
|
|
@ -17,6 +18,14 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (drawable->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(drawable->serial_number);
|
||||
if (cached) {
|
||||
return cached; // Already INCREF'd by lookup
|
||||
}
|
||||
}
|
||||
|
||||
PyTypeObject* type = nullptr;
|
||||
PyObject* obj = nullptr;
|
||||
|
||||
|
|
@ -28,6 +37,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIFrame>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -40,6 +50,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UICaption>(drawable);
|
||||
pyObj->font = nullptr;
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -51,6 +62,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUISpriteObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UISprite>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
@ -62,6 +74,7 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
|||
auto pyObj = (PyUIGridObject*)type->tp_alloc(type, 0);
|
||||
if (pyObj) {
|
||||
pyObj->data = std::static_pointer_cast<UIGrid>(drawable);
|
||||
pyObj->weakreflist = NULL;
|
||||
}
|
||||
obj = (PyObject*)pyObj;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,113 @@
|
|||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PythonObjectCache.h"
|
||||
|
||||
UIDrawable::UIDrawable() : position(0.0f, 0.0f) { click_callable = NULL; }
|
||||
|
||||
UIDrawable::UIDrawable(const UIDrawable& other)
|
||||
: z_index(other.z_index),
|
||||
name(other.name),
|
||||
position(other.position),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
serial_number(0), // Don't copy serial number
|
||||
use_render_texture(other.use_render_texture),
|
||||
render_dirty(true) // Force redraw after copy
|
||||
{
|
||||
// Deep copy click_callable if it exists
|
||||
if (other.click_callable) {
|
||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||
}
|
||||
|
||||
// Deep copy render texture if needed
|
||||
if (other.render_texture && other.use_render_texture) {
|
||||
auto size = other.render_texture->getSize();
|
||||
enableRenderTexture(size.x, size.y);
|
||||
}
|
||||
}
|
||||
|
||||
UIDrawable& UIDrawable::operator=(const UIDrawable& other) {
|
||||
if (this != &other) {
|
||||
// Copy basic members
|
||||
z_index = other.z_index;
|
||||
name = other.name;
|
||||
position = other.position;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
use_render_texture = other.use_render_texture;
|
||||
render_dirty = true; // Force redraw after copy
|
||||
|
||||
// Deep copy click_callable
|
||||
if (other.click_callable) {
|
||||
click_callable = std::make_unique<PyClickCallable>(*other.click_callable);
|
||||
} else {
|
||||
click_callable.reset();
|
||||
}
|
||||
|
||||
// Deep copy render texture if needed
|
||||
if (other.render_texture && other.use_render_texture) {
|
||||
auto size = other.render_texture->getSize();
|
||||
enableRenderTexture(size.x, size.y);
|
||||
} else {
|
||||
render_texture.reset();
|
||||
use_render_texture = false;
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UIDrawable::UIDrawable(UIDrawable&& other) noexcept
|
||||
: z_index(other.z_index),
|
||||
name(std::move(other.name)),
|
||||
position(other.position),
|
||||
visible(other.visible),
|
||||
opacity(other.opacity),
|
||||
serial_number(other.serial_number),
|
||||
click_callable(std::move(other.click_callable)),
|
||||
render_texture(std::move(other.render_texture)),
|
||||
render_sprite(std::move(other.render_sprite)),
|
||||
use_render_texture(other.use_render_texture),
|
||||
render_dirty(other.render_dirty)
|
||||
{
|
||||
// Clear the moved-from object's serial number to avoid cache issues
|
||||
other.serial_number = 0;
|
||||
}
|
||||
|
||||
UIDrawable& UIDrawable::operator=(UIDrawable&& other) noexcept {
|
||||
if (this != &other) {
|
||||
// Clear our own cache entry if we have one
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
|
||||
// Move basic members
|
||||
z_index = other.z_index;
|
||||
name = std::move(other.name);
|
||||
position = other.position;
|
||||
visible = other.visible;
|
||||
opacity = other.opacity;
|
||||
serial_number = other.serial_number;
|
||||
use_render_texture = other.use_render_texture;
|
||||
render_dirty = other.render_dirty;
|
||||
|
||||
// Move unique_ptr members
|
||||
click_callable = std::move(other.click_callable);
|
||||
render_texture = std::move(other.render_texture);
|
||||
render_sprite = std::move(other.render_sprite);
|
||||
|
||||
// Clear the moved-from object's serial number
|
||||
other.serial_number = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UIDrawable::~UIDrawable() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
void UIDrawable::click_unregister()
|
||||
{
|
||||
click_callable.reset();
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ public:
|
|||
void click_unregister();
|
||||
|
||||
UIDrawable();
|
||||
virtual ~UIDrawable();
|
||||
|
||||
// Copy constructor and assignment operator
|
||||
UIDrawable(const UIDrawable& other);
|
||||
UIDrawable& operator=(const UIDrawable& other);
|
||||
|
||||
// Move constructor and assignment operator
|
||||
UIDrawable(UIDrawable&& other) noexcept;
|
||||
UIDrawable& operator=(UIDrawable&& other) noexcept;
|
||||
|
||||
static PyObject* get_click(PyObject* self, void* closure);
|
||||
static int set_click(PyObject* self, PyObject* value, void* closure);
|
||||
|
|
@ -90,6 +99,9 @@ public:
|
|||
virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; }
|
||||
virtual bool getProperty(const std::string& name, std::string& value) const { return false; }
|
||||
|
||||
// Python object cache support
|
||||
uint64_t serial_number = 0;
|
||||
|
||||
protected:
|
||||
// RenderTexture support (opt-in)
|
||||
std::unique_ptr<sf::RenderTexture> render_texture;
|
||||
|
|
|
|||
154
src/UIEntity.cpp
154
src/UIEntity.cpp
|
|
@ -4,7 +4,7 @@
|
|||
#include <algorithm>
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
#include "UIEntityPyMethods.h"
|
||||
|
||||
|
|
@ -17,6 +17,12 @@ UIEntity::UIEntity()
|
|||
// gridstate vector starts empty - will be lazily initialized when needed
|
||||
}
|
||||
|
||||
UIEntity::~UIEntity() {
|
||||
if (serial_number != 0) {
|
||||
PythonObjectCache::getInstance().remove(serial_number);
|
||||
}
|
||||
}
|
||||
|
||||
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
|
||||
|
||||
void UIEntity::updateVisibility()
|
||||
|
|
@ -121,81 +127,57 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
|||
}
|
||||
|
||||
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
// Try parsing with PyArgHelpers for grid position
|
||||
int arg_idx = 0;
|
||||
auto grid_pos_result = PyArgHelpers::parseGridPosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float grid_x = 0.0f, grid_y = 0.0f;
|
||||
int sprite_index = 0;
|
||||
// Define all parameters with defaults
|
||||
PyObject* grid_pos_obj = nullptr;
|
||||
PyObject* texture = nullptr;
|
||||
int sprite_index = 0;
|
||||
PyObject* grid_obj = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got grid position from helpers (tuple format)
|
||||
if (grid_pos_result.valid) {
|
||||
grid_x = grid_pos_result.grid_x;
|
||||
grid_y = grid_pos_result.grid_y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"texture", "sprite_index", "grid", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OiO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&texture, &sprite_index, &grid_obj)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (grid_pos_result.error) PyErr_SetString(PyExc_TypeError, grid_pos_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"grid", "visible", "opacity", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast<char**>(kwlist),
|
||||
&grid_pos_obj, &texture, &sprite_index, // Positional
|
||||
&grid_obj, &visible, &opacity, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"grid_x", "grid_y", "texture", "sprite_index", "grid", "grid_pos", nullptr
|
||||
};
|
||||
PyObject* grid_pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
|
||||
const_cast<char**>(keywords),
|
||||
&grid_x, &grid_y, &texture, &sprite_index,
|
||||
&grid_obj, &grid_pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle grid_pos keyword override
|
||||
if (grid_pos_obj && grid_pos_obj != Py_None) {
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
grid_x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
grid_y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
}
|
||||
|
||||
// Handle grid position argument (can be tuple or use x/y keywords)
|
||||
if (grid_pos_obj) {
|
||||
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
||||
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
||||
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
||||
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
||||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// check types for texture
|
||||
//
|
||||
// Set Texture - allow None or use default
|
||||
//
|
||||
// Handle texture argument
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
if (texture && texture != Py_None) {
|
||||
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
}
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
|
|
@ -203,25 +185,33 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
// Allow creation without texture for testing purposes
|
||||
// if (!texture_ptr) {
|
||||
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
||||
// return -1;
|
||||
// }
|
||||
|
||||
if (grid_obj != NULL && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Handle grid argument
|
||||
if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Always use default constructor for lazy initialization
|
||||
// Create the entity
|
||||
self->data = std::make_shared<UIEntity>();
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Store reference to Python object
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
// Store reference to Python object (legacy - to be removed)
|
||||
self->data->self = (PyObject*)self;
|
||||
Py_INCREF(self);
|
||||
|
||||
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
|
||||
// Set texture and sprite index
|
||||
if (texture_ptr) {
|
||||
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
||||
} else {
|
||||
|
|
@ -230,12 +220,20 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
|
||||
// Set position using grid coordinates
|
||||
self->data->position = sf::Vector2f(grid_x, grid_y);
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
|
||||
if (grid_obj != NULL) {
|
||||
// Set other properties (delegate to sprite)
|
||||
self->data->sprite.visible = visible;
|
||||
self->data->sprite.opacity = opacity;
|
||||
if (name) {
|
||||
self->data->sprite.name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle grid attachment
|
||||
if (grid_obj) {
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
||||
self->data->grid = pygrid->data;
|
||||
// todone - on creation of Entity with Grid assignment, also append it to the entity list
|
||||
// Append entity to grid's entity list
|
||||
pygrid->data->entities->push_back(self->data);
|
||||
|
||||
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
||||
|
|
|
|||
|
|
@ -14,12 +14,37 @@
|
|||
#include "PyFont.h"
|
||||
|
||||
#include "UIGridPoint.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "UISprite.h"
|
||||
|
||||
class UIGrid;
|
||||
|
||||
// UIEntity
|
||||
/*
|
||||
|
||||
****************************************
|
||||
* say it with me: *
|
||||
* ✨ UIEntity is not a UIDrawable ✨ *
|
||||
****************************************
|
||||
|
||||
**Why Not, John?**
|
||||
Doesn't it say "UI" on the front of it?
|
||||
It sure does. Probably should have called it "GridEntity", but it's a bit late now.
|
||||
|
||||
UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...)
|
||||
|
||||
However:
|
||||
UIEntity has a position in **Grid tile coordinates**.
|
||||
UIEntity is not nestable at all. Grid -> Entity.
|
||||
UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection.
|
||||
UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites.
|
||||
UIEntity is, at its core, the container for *a perspective of map data*.
|
||||
The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data.
|
||||
|
||||
In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable.
|
||||
|
||||
*/
|
||||
|
||||
//class UIEntity;
|
||||
//typedef struct {
|
||||
// PyObject_HEAD
|
||||
|
|
@ -32,11 +57,11 @@ sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
|
|||
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
|
||||
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
|
||||
|
||||
// TODO: make UIEntity a drawable
|
||||
class UIEntity//: public UIDrawable
|
||||
class UIEntity
|
||||
{
|
||||
public:
|
||||
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
|
||||
uint64_t serial_number = 0; // For Python object cache
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::vector<UIGridPointState> gridstate;
|
||||
UISprite sprite;
|
||||
|
|
@ -44,6 +69,7 @@ public:
|
|||
//void render(sf::Vector2f); //override final;
|
||||
|
||||
UIEntity();
|
||||
~UIEntity();
|
||||
|
||||
// Visibility methods
|
||||
void updateVisibility(); // Update gridstate from current FOV
|
||||
|
|
@ -88,10 +114,31 @@ namespace mcrfpydef {
|
|||
.tp_itemsize = 0,
|
||||
.tp_repr = (reprfunc)UIEntity::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = "UIEntity objects",
|
||||
.tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
||||
"A game entity that exists on a grid with sprite rendering.\n\n"
|
||||
"Args:\n"
|
||||
" grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" texture (Texture, optional): Texture object for sprite. Default: default texture\n"
|
||||
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
|
||||
"Keyword Args:\n"
|
||||
" grid (Grid): Grid to attach entity to. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X grid position override. Default: 0\n"
|
||||
" y (float): Y grid position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" pos (tuple): Grid position as (x, y) tuple\n"
|
||||
" x, y (float): Grid position coordinates\n"
|
||||
" draw_pos (tuple): Pixel position for rendering\n"
|
||||
" gridstate (GridPointState): Visibility state for grid points\n"
|
||||
" sprite_index (int): Current sprite index\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" name (str): Element name"),
|
||||
.tp_methods = UIEntity_all_methods,
|
||||
.tp_getset = UIEntity::getsetters,
|
||||
.tp_base = &mcrfpydef::PyDrawableType,
|
||||
.tp_base = NULL,
|
||||
.tp_init = (initproc)UIEntity::init,
|
||||
.tp_new = PyType_GenericNew,
|
||||
};
|
||||
|
|
|
|||
197
src/UIFrame.cpp
197
src/UIFrame.cpp
|
|
@ -6,7 +6,7 @@
|
|||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
|
|
@ -432,67 +432,50 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
// Initialize children first
|
||||
self->data->children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
|
||||
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f, outline = 0.0f;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* outline_color = nullptr;
|
||||
float outline = 0.0f;
|
||||
PyObject* children_arg = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int clip_children = 0;
|
||||
|
||||
// Case 1: Got position and size from helpers (tuple format)
|
||||
if (pos_result.valid && size_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
w = size_result.w;
|
||||
h = size_result.h;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"fill_color", "outline_color", "outline", "children", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OOfOO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&fill_color, &outline_color, &outline,
|
||||
&children_arg, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
else if (size_result.error) PyErr_SetString(PyExc_TypeError, size_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "outline_color", "outline", "children", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "clip_children",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOfOOifizffffi", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, // Positional
|
||||
&fill_color, &outline_color, &outline, &children_arg, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &clip_children)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format (x, y, w, h, ...)
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"x", "y", "w", "h", "fill_color", "outline_color", "outline",
|
||||
"children", "click", "pos", "size", nullptr
|
||||
};
|
||||
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOOO",
|
||||
const_cast<char**>(keywords),
|
||||
&x, &y, &w, &h, &fill_color, &outline_color,
|
||||
&outline, &children_arg, &click_handler,
|
||||
&pos_obj, &size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (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);
|
||||
|
|
@ -500,47 +483,87 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size keyword override
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
}
|
||||
}
|
||||
// If no pos_obj but x/y keywords were provided, they're already in x, y variables
|
||||
|
||||
// Handle size argument (can be tuple or use w/h keywords)
|
||||
if (size_obj) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// If no size_obj but w/h keywords were provided, they're already in w, h variables
|
||||
|
||||
self->data->position = sf::Vector2f(x, y); // Set base class position
|
||||
self->data->box.setPosition(self->data->position); // Sync box position
|
||||
// Set the position and size
|
||||
self->data->position = sf::Vector2f(x, y);
|
||||
self->data->box.setPosition(self->data->position);
|
||||
self->data->box.setSize(sf::Vector2f(w, h));
|
||||
self->data->box.setOutlineThickness(outline);
|
||||
// getsetter abuse because I haven't standardized Color object parsing (TODO)
|
||||
int err_val = 0;
|
||||
if (fill_color && fill_color != Py_None) err_val = UIFrame::set_color_member(self, fill_color, (void*)0);
|
||||
else self->data->box.setFillColor(sf::Color(0,0,0,255));
|
||||
if (err_val) return err_val;
|
||||
if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1);
|
||||
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
|
||||
if (err_val) return err_val;
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->box.setFillColor(sf::Color(0, 0, 0, 128)); // Default: semi-transparent black
|
||||
}
|
||||
|
||||
// Handle outline_color
|
||||
if (outline_color && outline_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(outline_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "outline_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setOutlineColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
} else {
|
||||
self->data->box.setOutlineColor(sf::Color(255, 255, 255, 255)); // Default: white
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
self->data->clip_children = clip_children;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Process children argument if provided
|
||||
if (children_arg && children_arg != Py_None) {
|
||||
|
|
@ -605,6 +628,16 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUIFrameObject* obj = (PyUIFrameObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
obj->data.reset();
|
||||
Py_TYPE(self)->tp_free(self);
|
||||
},
|
||||
|
|
@ -85,28 +89,39 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Frame(x=0, y=0, w=0, h=0, fill_color=None, outline_color=None, outline=0, click=None, children=None)\n\n"
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR("Frame(pos=None, size=None, **kwargs)\n\n"
|
||||
"A rectangular frame UI element that can contain other drawable elements.\n\n"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" w (float): Width in pixels. Default: 0\n"
|
||||
" h (float): Height in pixels. Default: 0\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" size (tuple, optional): Size as (width, height) tuple. Default: (0, 0)\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Background fill color. Default: (0, 0, 0, 128)\n"
|
||||
" outline_color (Color): Border outline color. Default: (255, 255, 255, 255)\n"
|
||||
" outline (float): Border outline thickness. Default: 0\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" children (list): Initial list of child drawable elements. Default: None\n\n"
|
||||
" children (list): Initial list of child drawable elements. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" w (float): Width override. Default: 0\n"
|
||||
" h (float): Height override. Default: 0\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds. Default: False\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" fill_color, outline_color (Color): Visual appearance\n"
|
||||
" outline (float): Border thickness\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" children (list): Collection of child drawable elements\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" clip_children (bool): Whether to clip children to frame bounds"),
|
||||
.tp_methods = UIFrame_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
@ -116,7 +131,10 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUIFrameObject* self = (PyUIFrameObject*)type->tp_alloc(type, 0);
|
||||
if (self) self->data = std::make_shared<UIFrame>();
|
||||
if (self) {
|
||||
self->data = std::make_shared<UIFrame>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
255
src/UIGrid.cpp
255
src/UIGrid.cpp
|
|
@ -1,7 +1,7 @@
|
|||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include "PythonObjectCache.h"
|
||||
#include <algorithm>
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
|
|
@ -518,102 +518,49 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
|||
|
||||
|
||||
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
||||
// Default values
|
||||
int grid_x = 0, grid_y = 0;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
PyObject* textureObj = nullptr;
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
float center_x = 0.0f, center_y = 0.0f;
|
||||
float zoom = 1.0f;
|
||||
int perspective = -1; // perspective is a difficult __init__ arg; needs an entity in collection to work
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int grid_x = 2, grid_y = 2; // Default to 2x2 grid
|
||||
|
||||
// Check if first argument is a tuple (for tuple-based initialization)
|
||||
bool has_tuple_first_arg = false;
|
||||
if (args && PyTuple_Size(args) > 0) {
|
||||
PyObject* first_arg = PyTuple_GetItem(args, 0);
|
||||
if (PyTuple_Check(first_arg)) {
|
||||
has_tuple_first_arg = true;
|
||||
}
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "click", "center_x", "center_y", "zoom", "perspective",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffiifizffffii", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom, &perspective,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Try tuple-based parsing if we have a tuple as first argument
|
||||
if (has_tuple_first_arg) {
|
||||
int arg_idx = 0;
|
||||
auto grid_size_result = PyArgHelpers::parseGridSize(args, kwds, &arg_idx);
|
||||
|
||||
// If grid size parsing failed with an error, report it
|
||||
if (!grid_size_result.valid) {
|
||||
if (grid_size_result.error) {
|
||||
PyErr_SetString(PyExc_TypeError, grid_size_result.error);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Invalid grid size tuple");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// We got a valid grid size
|
||||
grid_x = grid_size_result.grid_w;
|
||||
grid_y = grid_size_result.grid_h;
|
||||
|
||||
// Try to parse position and size
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
}
|
||||
|
||||
auto size_result = PyArgHelpers::parseSize(args, kwds, &arg_idx);
|
||||
if (size_result.valid) {
|
||||
w = size_result.w;
|
||||
h = size_result.h;
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
// Default size based on grid dimensions
|
||||
w = grid_x * 16.0f;
|
||||
h = grid_y * 16.0f;
|
||||
}
|
||||
|
||||
// Parse remaining arguments (texture)
|
||||
static const char* remaining_keywords[] = { "texture", nullptr };
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|O",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&textureObj);
|
||||
Py_DECREF(remaining_args);
|
||||
}
|
||||
// Traditional format parsing
|
||||
else {
|
||||
static const char* keywords[] = {
|
||||
"grid_x", "grid_y", "texture", "pos", "size", "grid_size", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* size_obj = nullptr;
|
||||
PyObject* grid_size_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO",
|
||||
const_cast<char**>(keywords),
|
||||
&grid_x, &grid_y, &textureObj,
|
||||
&pos_obj, &size_obj, &grid_size_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle grid_size override
|
||||
if (grid_size_obj && grid_size_obj != Py_None) {
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1);
|
||||
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
|
||||
grid_x = PyLong_AsLong(x_obj);
|
||||
grid_y = PyLong_AsLong(y_obj);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must contain integers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple of two integers");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle position
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
PyErr_Clear();
|
||||
if (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);
|
||||
|
|
@ -622,36 +569,50 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must contain numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple of two numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size
|
||||
if (size_obj && size_obj != Py_None) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle size argument (can be tuple or use w/h keywords)
|
||||
if (size_obj) {
|
||||
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||
PyObject* w_val = PyTuple_GetItem(size_obj, 0);
|
||||
PyObject* h_val = PyTuple_GetItem(size_obj, 1);
|
||||
if ((PyFloat_Check(w_val) || PyLong_Check(w_val)) &&
|
||||
(PyFloat_Check(h_val) || PyLong_Check(h_val))) {
|
||||
w = PyFloat_Check(w_val) ? PyFloat_AsDouble(w_val) : PyLong_AsLong(w_val);
|
||||
h = PyFloat_Check(h_val) ? PyFloat_AsDouble(h_val) : PyLong_AsLong(h_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple of two numbers");
|
||||
PyErr_SetString(PyExc_TypeError, "size tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
// Default size based on grid
|
||||
w = grid_x * 16.0f;
|
||||
h = grid_y * 16.0f;
|
||||
PyErr_SetString(PyExc_TypeError, "size must be a tuple (w, h)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle grid_size argument (can be tuple or use grid_x/grid_y keywords)
|
||||
if (grid_size_obj) {
|
||||
if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) {
|
||||
PyObject* gx_val = PyTuple_GetItem(grid_size_obj, 0);
|
||||
PyObject* gy_val = PyTuple_GetItem(grid_size_obj, 1);
|
||||
if (PyLong_Check(gx_val) && PyLong_Check(gy_val)) {
|
||||
grid_x = PyLong_AsLong(gx_val);
|
||||
grid_y = PyLong_AsLong(gy_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple (grid_x, grid_y)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -661,12 +622,8 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
return -1;
|
||||
}
|
||||
|
||||
// At this point we have x, y, w, h values from either parsing method
|
||||
|
||||
// Convert PyObject texture to shared_ptr<PyTexture>
|
||||
// Handle texture argument
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
|
||||
// Allow None or NULL for texture - use default texture in that case
|
||||
if (textureObj && textureObj != Py_None) {
|
||||
if (!PyObject_IsInstance(textureObj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
|
|
@ -679,14 +636,64 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
// Adjust size based on texture if available and size not explicitly set
|
||||
if (texture_ptr && w == grid_x * 16.0f && h == grid_y * 16.0f) {
|
||||
// If size wasn't specified, calculate based on grid dimensions and texture
|
||||
if (!size_obj && texture_ptr) {
|
||||
w = grid_x * texture_ptr->sprite_width;
|
||||
h = grid_y * texture_ptr->sprite_height;
|
||||
} else if (!size_obj) {
|
||||
w = grid_x * 16.0f; // Default tile size
|
||||
h = grid_y * 16.0f;
|
||||
}
|
||||
|
||||
// Create the grid
|
||||
self->data = std::make_shared<UIGrid>(grid_x, grid_y, texture_ptr,
|
||||
sf::Vector2f(x, y), sf::Vector2f(w, h));
|
||||
|
||||
// Set additional properties
|
||||
self->data->center_x = center_x;
|
||||
self->data->center_y = center_y;
|
||||
self->data->zoom = zoom;
|
||||
self->data->perspective = perspective;
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Handle fill_color
|
||||
if (fill_color && fill_color != Py_None) {
|
||||
PyColorObject* color_obj = PyColor::from_arg(fill_color);
|
||||
if (!color_obj) {
|
||||
PyErr_SetString(PyExc_TypeError, "fill_color must be a Color or color tuple");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setFillColor(color_obj->data);
|
||||
Py_DECREF(color_obj);
|
||||
}
|
||||
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
return -1;
|
||||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
|
|
@ -1401,7 +1408,15 @@ PyObject* UIEntityCollection::getitem(PyUIEntityCollectionObject* self, Py_ssize
|
|||
std::advance(l_begin, index);
|
||||
auto target = *l_begin; //auto target = (*vec)[index];
|
||||
|
||||
// If the entity has a stored Python object reference, return that to preserve derived class
|
||||
// Check cache first to preserve derived class
|
||||
if (target->serial_number != 0) {
|
||||
PyObject* cached = PythonObjectCache::getInstance().lookup(target->serial_number);
|
||||
if (cached) {
|
||||
return cached; // Already INCREF'd by lookup
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy: If the entity has a stored Python object reference, return that to preserve derived class
|
||||
if (target->self != nullptr) {
|
||||
Py_INCREF(target->self);
|
||||
return target->self;
|
||||
|
|
|
|||
70
src/UIGrid.h
70
src/UIGrid.h
|
|
@ -172,41 +172,65 @@ namespace mcrfpydef {
|
|||
.tp_name = "mcrfpy.Grid",
|
||||
.tp_basicsize = sizeof(PyUIGridObject),
|
||||
.tp_itemsize = 0,
|
||||
//.tp_dealloc = (destructor)[](PyObject* self)
|
||||
//{
|
||||
// PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
// obj->data.reset();
|
||||
// Py_TYPE(self)->tp_free(self);
|
||||
//},
|
||||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUIGridObject* obj = (PyUIGridObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
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_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Grid(x=0, y=0, grid_size=(20, 20), texture=None, tile_width=16, tile_height=16, scale=1.0, click=None)\n\n"
|
||||
"A grid-based tilemap UI element for rendering tile-based levels and game worlds.\n\n"
|
||||
.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"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" grid_size (tuple): Grid dimensions as (width, height) in tiles. Default: (20, 20)\n"
|
||||
" texture (Texture): Texture atlas containing tile sprites. Default: None\n"
|
||||
" tile_width (int): Width of each tile in pixels. Default: 16\n"
|
||||
" tile_height (int): Height of each tile in pixels. Default: 16\n"
|
||||
" scale (float): Grid scaling factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" size (tuple, optional): Size as (width, height) tuple. Default: auto-calculated from grid_size\n"
|
||||
" grid_size (tuple, optional): Grid dimensions as (grid_x, grid_y) tuple. Default: (2, 2)\n"
|
||||
" texture (Texture, optional): Texture containing tile sprites. Default: default texture\n\n"
|
||||
"Keyword Args:\n"
|
||||
" fill_color (Color): Background fill color. Default: None\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" center_x (float): X coordinate of center point. Default: 0\n"
|
||||
" center_y (float): Y coordinate of center point. Default: 0\n"
|
||||
" zoom (float): Zoom level for rendering. Default: 1.0\n"
|
||||
" perspective (int): Entity perspective index (-1 for omniscient). Default: -1\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n"
|
||||
" w (float): Width override. Default: auto-calculated\n"
|
||||
" h (float): Height override. Default: auto-calculated\n"
|
||||
" grid_x (int): Grid width override. Default: 2\n"
|
||||
" grid_y (int): Grid height override. Default: 2\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" w, h (float): Size in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" size (tuple): Size as (width, height) tuple\n"
|
||||
" center (tuple): Center point as (x, y) tuple\n"
|
||||
" center_x, center_y (float): Center point coordinates\n"
|
||||
" zoom (float): Zoom level for rendering\n"
|
||||
" grid_size (tuple): Grid dimensions (width, height) in tiles\n"
|
||||
" tile_width, tile_height (int): Tile dimensions in pixels\n"
|
||||
" grid_x, grid_y (int): Grid dimensions\n"
|
||||
" texture (Texture): Tile texture atlas\n"
|
||||
" scale (float): Scale multiplier\n"
|
||||
" points (list): 2D array of GridPoint objects for tile data\n"
|
||||
" entities (list): Collection of Entity objects in the grid\n"
|
||||
" background_color (Color): Grid background color\n"
|
||||
" fill_color (Color): Background color\n"
|
||||
" entities (EntityCollection): Collection of entities in the grid\n"
|
||||
" perspective (int): Entity perspective index\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" z_index (int): Rendering order"),
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name"),
|
||||
.tp_methods = UIGrid_all_methods,
|
||||
//.tp_members = UIGrid::members,
|
||||
.tp_getset = UIGrid::getsetters,
|
||||
|
|
|
|||
171
src/UISprite.cpp
171
src/UISprite.cpp
|
|
@ -1,7 +1,7 @@
|
|||
#include "UISprite.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyArgHelpers.h"
|
||||
#include "PythonObjectCache.h"
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
|
|
@ -29,6 +29,42 @@ UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vect
|
|||
sprite = ptex->sprite(sprite_index, position, sf::Vector2f(_scale, _scale));
|
||||
}
|
||||
|
||||
UISprite::UISprite(const UISprite& other)
|
||||
: UIDrawable(other),
|
||||
sprite_index(other.sprite_index),
|
||||
sprite(other.sprite),
|
||||
ptex(other.ptex)
|
||||
{
|
||||
}
|
||||
|
||||
UISprite& UISprite::operator=(const UISprite& other) {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(other);
|
||||
sprite_index = other.sprite_index;
|
||||
sprite = other.sprite;
|
||||
ptex = other.ptex;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
UISprite::UISprite(UISprite&& other) noexcept
|
||||
: UIDrawable(std::move(other)),
|
||||
sprite_index(other.sprite_index),
|
||||
sprite(std::move(other.sprite)),
|
||||
ptex(std::move(other.ptex))
|
||||
{
|
||||
}
|
||||
|
||||
UISprite& UISprite::operator=(UISprite&& other) noexcept {
|
||||
if (this != &other) {
|
||||
UIDrawable::operator=(std::move(other));
|
||||
sprite_index = other.sprite_index;
|
||||
sprite = std::move(other.sprite);
|
||||
ptex = std::move(other.ptex);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/*
|
||||
void UISprite::render(sf::Vector2f offset)
|
||||
{
|
||||
|
|
@ -327,57 +363,46 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
|
|||
|
||||
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||
{
|
||||
// Try parsing with PyArgHelpers
|
||||
int arg_idx = 0;
|
||||
auto pos_result = PyArgHelpers::parsePosition(args, kwds, &arg_idx);
|
||||
|
||||
// Default values
|
||||
float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||
int sprite_index = 0;
|
||||
// Define all parameters with defaults
|
||||
PyObject* pos_obj = nullptr;
|
||||
PyObject* texture = nullptr;
|
||||
int sprite_index = 0;
|
||||
float scale = 1.0f;
|
||||
float scale_x = 1.0f;
|
||||
float scale_y = 1.0f;
|
||||
PyObject* click_handler = nullptr;
|
||||
int visible = 1;
|
||||
float opacity = 1.0f;
|
||||
int z_index = 0;
|
||||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
|
||||
// Case 1: Got position from helpers (tuple format)
|
||||
if (pos_result.valid) {
|
||||
x = pos_result.x;
|
||||
y = pos_result.y;
|
||||
|
||||
// Parse remaining arguments
|
||||
static const char* remaining_keywords[] = {
|
||||
"texture", "sprite_index", "scale", "click", nullptr
|
||||
};
|
||||
|
||||
// Create new tuple with remaining args
|
||||
Py_ssize_t total_args = PyTuple_Size(args);
|
||||
PyObject* remaining_args = PyTuple_GetSlice(args, arg_idx, total_args);
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(remaining_args, kwds, "|OifO",
|
||||
const_cast<char**>(remaining_keywords),
|
||||
&texture, &sprite_index, &scale, &click_handler)) {
|
||||
Py_DECREF(remaining_args);
|
||||
if (pos_result.error) PyErr_SetString(PyExc_TypeError, pos_result.error);
|
||||
return -1;
|
||||
}
|
||||
Py_DECREF(remaining_args);
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "texture", "sprite_index", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"scale", "scale_x", "scale_y", "click",
|
||||
"visible", "opacity", "z_index", "name", "x", "y",
|
||||
nullptr
|
||||
};
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifffOifizff", const_cast<char**>(kwlist),
|
||||
&pos_obj, &texture, &sprite_index, // Positional
|
||||
&scale, &scale_x, &scale_y, &click_handler,
|
||||
&visible, &opacity, &z_index, &name, &x, &y)) {
|
||||
return -1;
|
||||
}
|
||||
// Case 2: Traditional format
|
||||
else {
|
||||
PyErr_Clear(); // Clear any errors from helpers
|
||||
|
||||
static const char* keywords[] = {
|
||||
"x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr
|
||||
};
|
||||
PyObject* pos_obj = nullptr;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
|
||||
const_cast<char**>(keywords),
|
||||
&x, &y, &texture, &sprite_index, &scale,
|
||||
&click_handler, &pos_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle pos keyword override
|
||||
if (pos_obj && pos_obj != Py_None) {
|
||||
|
||||
// Handle position argument (can be tuple, Vector, or use x/y keywords)
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (vec) {
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
Py_DECREF(vec);
|
||||
} else {
|
||||
PyErr_Clear();
|
||||
if (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);
|
||||
|
|
@ -385,12 +410,10 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
||||
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
||||
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos tuple must contain numbers");
|
||||
return -1;
|
||||
}
|
||||
} else if (PyObject_TypeCheck(pos_obj, (PyTypeObject*)PyObject_GetAttrString(
|
||||
PyImport_ImportModule("mcrfpy"), "Vector"))) {
|
||||
PyVectorObject* vec = (PyVectorObject*)pos_obj;
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y) or Vector");
|
||||
return -1;
|
||||
|
|
@ -400,10 +423,11 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
|
||||
// Handle texture - allow None or use default
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
if (texture && texture != Py_None) {
|
||||
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
}
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
|
|
@ -416,9 +440,27 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
return -1;
|
||||
}
|
||||
|
||||
// Create the sprite
|
||||
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
||||
|
||||
// Set scale properties
|
||||
if (scale_x != 1.0f || scale_y != 1.0f) {
|
||||
// If scale_x or scale_y were explicitly set, use them
|
||||
self->data->setScale(sf::Vector2f(scale_x, scale_y));
|
||||
} else if (scale != 1.0f) {
|
||||
// Otherwise use uniform scale
|
||||
self->data->setScale(sf::Vector2f(scale, scale));
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
self->data->visible = visible;
|
||||
self->data->opacity = opacity;
|
||||
self->data->z_index = z_index;
|
||||
if (name) {
|
||||
self->data->name = std::string(name);
|
||||
}
|
||||
|
||||
// Process click handler if provided
|
||||
// Handle click handler
|
||||
if (click_handler && click_handler != Py_None) {
|
||||
if (!PyCallable_Check(click_handler)) {
|
||||
PyErr_SetString(PyExc_TypeError, "click must be callable");
|
||||
|
|
@ -427,6 +469,19 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
// Register in Python object cache
|
||||
if (self->data->serial_number == 0) {
|
||||
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
||||
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
||||
if (weakref) {
|
||||
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
||||
Py_DECREF(weakref); // Cache owns the reference now
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@ protected:
|
|||
public:
|
||||
UISprite();
|
||||
UISprite(std::shared_ptr<PyTexture>, int, sf::Vector2f, float);
|
||||
|
||||
// Copy constructor and assignment operator
|
||||
UISprite(const UISprite& other);
|
||||
UISprite& operator=(const UISprite& other);
|
||||
|
||||
// Move constructor and assignment operator
|
||||
UISprite(UISprite&& other) noexcept;
|
||||
UISprite& operator=(UISprite&& other) noexcept;
|
||||
void update();
|
||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||
virtual UIDrawable* click_at(sf::Vector2f point) override final;
|
||||
|
|
@ -82,6 +90,10 @@ namespace mcrfpydef {
|
|||
.tp_dealloc = (destructor)[](PyObject* self)
|
||||
{
|
||||
PyUISpriteObject* obj = (PyUISpriteObject*)self;
|
||||
// Clear weak references
|
||||
if (obj->weakreflist != NULL) {
|
||||
PyObject_ClearWeakRefs(self);
|
||||
}
|
||||
// release reference to font object
|
||||
//if (obj->texture) Py_DECREF(obj->texture);
|
||||
obj->data.reset();
|
||||
|
|
@ -91,24 +103,36 @@ namespace mcrfpydef {
|
|||
//.tp_hash = NULL,
|
||||
//.tp_iter
|
||||
//.tp_iternext
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Sprite(x=0, y=0, texture=None, sprite_index=0, scale=1.0, click=None)\n\n"
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||
.tp_doc = PyDoc_STR("Sprite(pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
|
||||
"A sprite UI element that displays a texture or portion of a texture atlas.\n\n"
|
||||
"Args:\n"
|
||||
" x (float): X position in pixels. Default: 0\n"
|
||||
" y (float): Y position in pixels. Default: 0\n"
|
||||
" texture (Texture): Texture object to display. Default: None\n"
|
||||
" sprite_index (int): Index into texture atlas (if applicable). Default: 0\n"
|
||||
" scale (float): Sprite scaling factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n\n"
|
||||
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||
" texture (Texture, optional): Texture object to display. Default: default texture\n"
|
||||
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
|
||||
"Keyword Args:\n"
|
||||
" scale (float): Uniform scale factor. Default: 1.0\n"
|
||||
" scale_x (float): Horizontal scale factor. Default: 1.0\n"
|
||||
" scale_y (float): Vertical scale factor. Default: 1.0\n"
|
||||
" click (callable): Click event handler. Default: None\n"
|
||||
" visible (bool): Visibility state. Default: True\n"
|
||||
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
|
||||
" z_index (int): Rendering order. Default: 0\n"
|
||||
" name (str): Element name for finding. Default: None\n"
|
||||
" x (float): X position override. Default: 0\n"
|
||||
" y (float): Y position override. Default: 0\n\n"
|
||||
"Attributes:\n"
|
||||
" x, y (float): Position in pixels\n"
|
||||
" pos (Vector): Position as a Vector object\n"
|
||||
" texture (Texture): The texture being displayed\n"
|
||||
" sprite_index (int): Current sprite index in texture atlas\n"
|
||||
" scale (float): Scale multiplier\n"
|
||||
" scale (float): Uniform scale factor\n"
|
||||
" scale_x, scale_y (float): Individual scale factors\n"
|
||||
" click (callable): Click event handler\n"
|
||||
" visible (bool): Visibility state\n"
|
||||
" opacity (float): Opacity value\n"
|
||||
" z_index (int): Rendering order\n"
|
||||
" name (str): Element name\n"
|
||||
" w, h (float): Read-only computed size based on texture and scale"),
|
||||
.tp_methods = UISprite_methods,
|
||||
//.tp_members = PyUIFrame_members,
|
||||
|
|
@ -118,7 +142,10 @@ namespace mcrfpydef {
|
|||
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||
{
|
||||
PyUISpriteObject* self = (PyUISpriteObject*)type->tp_alloc(type, 0);
|
||||
//if (self) self->data = std::make_shared<UICaption>();
|
||||
if (self) {
|
||||
self->data = std::make_shared<UISprite>();
|
||||
self->weakreflist = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -67,10 +67,10 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
self.draw_pos = (tx, ty)
|
||||
for e in self.game.entities:
|
||||
if e is self: continue
|
||||
if e.draw_pos == old_pos: e.ev_exit(self)
|
||||
if e.draw_pos.x == old_pos.x and e.draw_pos.y == old_pos.y: e.ev_exit(self)
|
||||
for e in self.game.entities:
|
||||
if e is self: continue
|
||||
if e.draw_pos == (tx, ty): e.ev_enter(self)
|
||||
if e.draw_pos.x == tx and e.draw_pos.y == ty: e.ev_enter(self)
|
||||
|
||||
def act(self):
|
||||
pass
|
||||
|
|
@ -83,12 +83,12 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
|
||||
def try_move(self, dx, dy, test=False):
|
||||
x_max, y_max = self.grid.grid_size
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
#for e in iterable_entities(self.grid):
|
||||
|
||||
# sorting entities to test against the boulder instead of the button when they overlap.
|
||||
for e in sorted(self.game.entities, key = lambda i: i.draw_order, reverse = True):
|
||||
if e.draw_pos == (tx, ty):
|
||||
if e.draw_pos.x == tx and e.draw_pos.y == ty:
|
||||
#print(f"bumping {e}")
|
||||
return e.bump(self, dx, dy)
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ class COSEntity(): #mcrfpy.Entity): # Fake mcrfpy.Entity integration; engine bu
|
|||
return False
|
||||
|
||||
def _relative_move(self, dx, dy):
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
#self.draw_pos = (tx, ty)
|
||||
self.do_move(tx, ty)
|
||||
|
||||
|
|
@ -181,7 +181,7 @@ class Equippable:
|
|||
if self.zap_cooldown_remaining != 0:
|
||||
print("zap is cooling down.")
|
||||
return False
|
||||
fx, fy = caster.draw_pos
|
||||
fx, fy = caster.draw_pos.x, caster.draw_pos.y
|
||||
x, y = int(fx), int (fy)
|
||||
dist = lambda tx, ty: abs(int(tx) - x) + abs(int(ty) - y)
|
||||
targets = []
|
||||
|
|
@ -293,7 +293,7 @@ class PlayerEntity(COSEntity):
|
|||
## TODO - find other entities to avoid spawning on top of
|
||||
for spawn in spawn_points:
|
||||
for e in avoid or []:
|
||||
if e.draw_pos == spawn: break
|
||||
if e.draw_pos.x == spawn[0] and e.draw_pos.y == spawn[1]: break
|
||||
else:
|
||||
break
|
||||
self.draw_pos = spawn
|
||||
|
|
@ -314,9 +314,9 @@ class BoulderEntity(COSEntity):
|
|||
elif type(other) == EnemyEntity:
|
||||
if not other.can_push: return False
|
||||
#tx, ty = int(self.e.position[0] + dx), int(self.e.position[1] + dy)
|
||||
tx, ty = int(self.draw_pos[0] + dx), int(self.draw_pos[1] + dy)
|
||||
tx, ty = int(self.draw_pos.x + dx), int(self.draw_pos.y + dy)
|
||||
# Is the boulder blocked the same direction as the bumper? If not, let's both move
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
if self.try_move(dx, dy, test=test):
|
||||
if not test:
|
||||
other.do_move(*old_pos)
|
||||
|
|
@ -342,7 +342,7 @@ class ButtonEntity(COSEntity):
|
|||
# self.exit.unlock()
|
||||
# TODO: unlock, and then lock again, when player steps on/off
|
||||
if not test:
|
||||
pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
other.do_move(*pos)
|
||||
return True
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ class EnemyEntity(COSEntity):
|
|||
def bump(self, other, dx, dy, test=False):
|
||||
if self.hp == 0:
|
||||
if not test:
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
other.do_move(*old_pos)
|
||||
return True
|
||||
if type(other) == PlayerEntity:
|
||||
|
|
@ -415,7 +415,7 @@ class EnemyEntity(COSEntity):
|
|||
print("Ouch, my entire body!!")
|
||||
self._entity.sprite_number = self.base_sprite + 246
|
||||
self.hp = 0
|
||||
old_pos = int(self.draw_pos[0]), int(self.draw_pos[1])
|
||||
old_pos = int(self.draw_pos.x), int(self.draw_pos.y)
|
||||
if not test:
|
||||
other.do_move(*old_pos)
|
||||
return True
|
||||
|
|
@ -423,8 +423,8 @@ class EnemyEntity(COSEntity):
|
|||
def act(self):
|
||||
if self.hp > 0:
|
||||
# if player nearby: attack
|
||||
x, y = self.draw_pos
|
||||
px, py = self.game.player.draw_pos
|
||||
x, y = self.draw_pos.x, self.draw_pos.y
|
||||
px, py = self.game.player.draw_pos.x, self.game.player.draw_pos.y
|
||||
for d in ((1, 0), (0, 1), (-1, 0), (1, 0)):
|
||||
if int(x + d[0]) == int(px) and int(y + d[1]) == int(py):
|
||||
self.try_move(*d)
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ class TileInfo:
|
|||
@staticmethod
|
||||
def from_grid(grid, xy:tuple):
|
||||
values = {}
|
||||
x_max, y_max = grid.grid_size
|
||||
for d in deltas:
|
||||
tx, ty = d[0] + xy[0], d[1] + xy[1]
|
||||
try:
|
||||
values[d] = grid.at((tx, ty)).walkable
|
||||
except ValueError:
|
||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||
values[d] = True
|
||||
else:
|
||||
values[d] = grid.at((tx, ty)).walkable
|
||||
return TileInfo(values)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -70,10 +71,10 @@ def special_rule_verify(rule, grid, xy, unverified_tiles, pass_unverified=False)
|
|||
tx, ty = xy[0] + dxy[0], xy[1] + dxy[1]
|
||||
#print(f"Special rule: {cardinal} {allowed_tile} {type(allowed_tile)} -> ({tx}, {ty}) [{grid.at((tx, ty)).tilesprite}]{'*' if (tx, ty) in unverified_tiles else ''}")
|
||||
if (tx, ty) in unverified_tiles and cardinal in "nsew": return pass_unverified
|
||||
try:
|
||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||
except ValueError:
|
||||
x_max, y_max = grid.grid_size
|
||||
if tx < 0 or tx >= x_max or ty < 0 or ty >= y_max:
|
||||
return False
|
||||
return grid.at((tx, ty)).tilesprite == allowed_tile
|
||||
|
||||
import random
|
||||
tile_of_last_resort = 431
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class Crypt:
|
|||
|
||||
# Side Bar (inventory, level info) config
|
||||
self.level_caption = mcrfpy.Caption((5,5), "Level: 1", font, fill_color=(255, 255, 255))
|
||||
self.level_caption.size = 26
|
||||
self.level_caption.font_size = 26
|
||||
self.level_caption.outline = 3
|
||||
self.level_caption.outline_color = (0, 0, 0)
|
||||
self.sidebar.children.append(self.level_caption)
|
||||
|
|
@ -103,7 +103,7 @@ class Crypt:
|
|||
mcrfpy.Caption((25, 130 + 95 * i), "x", font, fill_color=(255, 255, 255)) for i in range(5)
|
||||
]
|
||||
for i in self.inv_captions:
|
||||
i.size = 16
|
||||
i.font_size = 16
|
||||
self.sidebar.children.append(i)
|
||||
|
||||
liminal_void = mcrfpy.Grid(1, 1, t, (0, 0), (16, 16))
|
||||
|
|
@ -382,7 +382,7 @@ class Crypt:
|
|||
def pull_boulder_search(self):
|
||||
for dx, dy in ( (0, -1), (-1, 0), (1, 0), (0, 1) ):
|
||||
for e in self.entities:
|
||||
if e.draw_pos != (self.player.draw_pos[0] + dx, self.player.draw_pos[1] + dy): continue
|
||||
if e.draw_pos.x != self.player.draw_pos.x + dx or e.draw_pos.y != self.player.draw_pos.y + dy: continue
|
||||
if type(e) == ce.BoulderEntity:
|
||||
self.pull_boulder_move((dx, dy), e)
|
||||
return self.enemy_turn()
|
||||
|
|
@ -395,7 +395,7 @@ class Crypt:
|
|||
if self.player.try_move(-p[0], -p[1], test=True):
|
||||
old_pos = self.player.draw_pos
|
||||
self.player.try_move(-p[0], -p[1])
|
||||
target_boulder.do_move(*old_pos)
|
||||
target_boulder.do_move(old_pos.x, old_pos.y)
|
||||
|
||||
def swap_level(self, new_level, spawn_point):
|
||||
self.level = new_level
|
||||
|
|
@ -451,7 +451,7 @@ class SweetButton:
|
|||
|
||||
# main button caption
|
||||
self.caption = mcrfpy.Caption((40, 3), caption, font, fill_color=font_color)
|
||||
self.caption.size = font_size
|
||||
self.caption.font_size = font_size
|
||||
self.caption.outline_color=font_outline_color
|
||||
self.caption.outline=font_outline_width
|
||||
self.main_button.children.append(self.caption)
|
||||
|
|
@ -548,20 +548,20 @@ class MainMenu:
|
|||
# title text
|
||||
drop_shadow = mcrfpy.Caption((150, 10), "Crypt Of Sokoban", font, fill_color=(96, 96, 96), outline_color=(192, 0, 0))
|
||||
drop_shadow.outline = 3
|
||||
drop_shadow.size = 64
|
||||
drop_shadow.font_size = 64
|
||||
components.append(
|
||||
drop_shadow
|
||||
)
|
||||
|
||||
title_txt = mcrfpy.Caption((158, 18), "Crypt Of Sokoban", font, fill_color=(255, 255, 255))
|
||||
title_txt.size = 64
|
||||
title_txt.font_size = 64
|
||||
components.append(
|
||||
title_txt
|
||||
)
|
||||
|
||||
# toast: text over the demo grid that fades out on a timer
|
||||
self.toast = mcrfpy.Caption((150, 400), "", font, fill_color=(0, 0, 0))
|
||||
self.toast.size = 28
|
||||
self.toast.font_size = 28
|
||||
self.toast.outline = 2
|
||||
self.toast.outline_color = (255, 255, 255)
|
||||
self.toast_event = None
|
||||
|
|
@ -626,6 +626,7 @@ class MainMenu:
|
|||
def play(self, sweet_btn, args):
|
||||
#if args[3] == "start": return # DRAMATIC on release action!
|
||||
if args[3] == "end": return
|
||||
mcrfpy.delTimer("demo_motion") # Clean up the demo timer
|
||||
self.crypt = Crypt()
|
||||
#mcrfpy.setScene("play")
|
||||
self.crypt.start()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue