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.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-07-12 10:21:48 -04:00
parent 98fc49a978
commit 062e4dadc4
5 changed files with 180 additions and 94 deletions

View File

@ -23,60 +23,60 @@ Animation::Animation(const std::string& targetProperty,
{ {
} }
void Animation::start(UIDrawable* target) { void Animation::start(std::shared_ptr<UIDrawable> target) {
currentTarget = target; if (!target) return;
targetWeak = target;
elapsed = 0.0f; elapsed = 0.0f;
// Capture startValue from target based on targetProperty // Capture start value from target
if (!currentTarget) return; std::visit([this, &target](const auto& targetVal) {
// Try to get the current value based on the expected type
std::visit([this](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>; using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) { if constexpr (std::is_same_v<T, float>) {
float value; float value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
else if constexpr (std::is_same_v<T, int>) { else if constexpr (std::is_same_v<T, int>) {
int value; int value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
else if constexpr (std::is_same_v<T, std::vector<int>>) { else if constexpr (std::is_same_v<T, std::vector<int>>) {
// For sprite animation, get current sprite index // For sprite animation, get current sprite index
int value; int value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
else if constexpr (std::is_same_v<T, sf::Color>) { else if constexpr (std::is_same_v<T, sf::Color>) {
sf::Color value; sf::Color value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
else if constexpr (std::is_same_v<T, sf::Vector2f>) { else if constexpr (std::is_same_v<T, sf::Vector2f>) {
sf::Vector2f value; sf::Vector2f value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
else if constexpr (std::is_same_v<T, std::string>) { else if constexpr (std::is_same_v<T, std::string>) {
std::string value; std::string value;
if (currentTarget->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
startValue = value; startValue = value;
} }
} }
}, targetValue); }, targetValue);
} }
void Animation::startEntity(UIEntity* target) { void Animation::startEntity(std::shared_ptr<UIEntity> target) {
currentEntityTarget = target; if (!target) return;
currentTarget = nullptr; // Clear drawable target
entityTargetWeak = target;
elapsed = 0.0f; elapsed = 0.0f;
// Capture the starting value from the entity // Capture the starting value from the entity
@ -99,8 +99,36 @@ void Animation::startEntity(UIEntity* target) {
}, targetValue); }, targetValue);
} }
bool Animation::hasValidTarget() const {
return !targetWeak.expired() || !entityTargetWeak.expired();
}
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) { 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; return false;
} }
@ -114,39 +142,12 @@ bool Animation::update(float deltaTime) {
// Get interpolated value // Get interpolated value
AnimationValue currentValue = interpolate(easedT); AnimationValue currentValue = interpolate(easedT);
// Apply currentValue to target (either drawable or entity) // Apply to whichever target is valid
std::visit([this](const auto& value) { if (target) {
using T = std::decay_t<decltype(value)>; applyValue(target.get(), currentValue);
} else if (entity) {
if (currentTarget) { applyValue(entity.get(), currentValue);
// 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);
return !isComplete(); return !isComplete();
} }
@ -254,6 +255,46 @@ AnimationValue Animation::interpolate(float t) const {
}, targetValue); }, 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);
}
// Easing functions implementation // Easing functions implementation
namespace EasingFunctions { namespace EasingFunctions {
@ -502,26 +543,31 @@ AnimationManager& AnimationManager::getInstance() {
} }
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) { void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
if (animation && animation->hasValidTarget()) {
activeAnimations.push_back(animation); activeAnimations.push_back(animation);
} }
}
void AnimationManager::update(float deltaTime) { void AnimationManager::update(float deltaTime) {
for (auto& anim : activeAnimations) { // Remove completed or invalid animations
anim->update(deltaTime);
}
cleanup();
}
void AnimationManager::cleanup() {
activeAnimations.erase( activeAnimations.erase(
std::remove_if(activeAnimations.begin(), activeAnimations.end(), std::remove_if(activeAnimations.begin(), activeAnimations.end(),
[](const std::shared_ptr<Animation>& anim) { [deltaTime](std::shared_ptr<Animation>& anim) {
return anim->isComplete(); return !anim || !anim->update(deltaTime);
}), }),
activeAnimations.end() activeAnimations.end()
); );
} }
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(); activeAnimations.clear();
} }

View File

@ -39,10 +39,13 @@ public:
bool delta = false); bool delta = false);
// Apply this animation to a drawable // 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) // 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) // Update animation (called each frame)
// Returns true if animation is still running, false if complete // Returns true if animation is still running, false if complete
@ -51,6 +54,9 @@ public:
// Get current interpolated value // Get current interpolated value
AnimationValue getCurrentValue() const; AnimationValue getCurrentValue() const;
// Check if animation has valid target
bool hasValidTarget() const;
// Animation properties // Animation properties
std::string getTargetProperty() const { return targetProperty; } std::string getTargetProperty() const { return targetProperty; }
float getDuration() const { return duration; } float getDuration() const { return duration; }
@ -67,11 +73,16 @@ private:
EasingFunction easingFunc; // Easing function to use EasingFunction easingFunc; // Easing function to use
bool delta; // If true, targetValue is relative to start bool delta; // If true, targetValue is relative to start
UIDrawable* currentTarget = nullptr; // Current target being animated // RAII: Use weak_ptr for safe target tracking
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable) std::weak_ptr<UIDrawable> targetWeak;
std::weak_ptr<UIEntity> entityTargetWeak;
// Helper to interpolate between values // Helper to interpolate between values
AnimationValue interpolate(float t) const; 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);
}; };
// Easing functions library // Easing functions library
@ -134,11 +145,8 @@ public:
// Update all animations // Update all animations
void update(float deltaTime); void update(float deltaTime);
// Remove completed animations // Clear all animations (optionally completing them first)
void cleanup(); void clear(bool completeAnimations = false);
// Clear all animations
void clear();
private: private:
AnimationManager() = default; AnimationManager() = default;

View File

@ -16,7 +16,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
{ {
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf"); Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
Resources::game = this; Resources::game = this;
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine"; window_title = "McRogueFace Engine";
// Initialize rendering based on headless mode // Initialize rendering based on headless mode
if (headless) { if (headless) {
@ -91,6 +91,9 @@ void GameEngine::cleanup()
if (cleaned_up) return; if (cleaned_up) return;
cleaned_up = true; cleaned_up = true;
// Clear all animations first (RAII handles invalidation)
AnimationManager::getInstance().clear();
// Clear Python references before destroying C++ objects // Clear Python references before destroying C++ objects
// Clear all timers (they hold Python callables) // Clear all timers (they hold Python callables)
timers.clear(); timers.clear();
@ -182,7 +185,7 @@ void GameEngine::setWindowScale(float multiplier)
void GameEngine::run() 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; float fps = 0.0;
frameTime = 0.016f; // Initialize to ~60 FPS frameTime = 0.016f; // Initialize to ~60 FPS
clock.restart(); clock.restart();
@ -259,7 +262,7 @@ void GameEngine::run()
int tenth_fps = (metrics.fps * 10) % 10; int tenth_fps = (metrics.fps * 10) % 10;
if (!headless && window) { 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 // In windowed mode, check if window was closed

View File

@ -126,50 +126,50 @@ PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
return NULL; return NULL;
} }
// Get the UIDrawable from the Python object
UIDrawable* drawable = nullptr;
// Check type by comparing type names // Check type by comparing type names
const char* type_name = Py_TYPE(target_obj)->tp_name; const char* type_name = Py_TYPE(target_obj)->tp_name;
if (strcmp(type_name, "mcrfpy.Frame") == 0) { if (strcmp(type_name, "mcrfpy.Frame") == 0) {
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; 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) { else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; 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) { else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; 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) { else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
PyUIGridObject* grid = (PyUIGridObject*)target_obj; 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) { else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
// Special handling for Entity since it doesn't inherit from UIDrawable // Special handling for Entity since it doesn't inherit from UIDrawable
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
// Start the animation directly on the entity if (entity->data) {
self->data->startEntity(entity->data.get()); self->data->startEntity(entity->data);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data); AnimationManager::getInstance().addAnimation(self->data);
}
Py_RETURN_NONE;
} }
else { else {
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
return NULL; return NULL;
} }
// Start the animation
self->data->start(drawable);
// Add to AnimationManager
AnimationManager::getInstance().addAnimation(self->data);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -214,6 +214,20 @@ PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args
}, value); }, 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[] = { PyGetSetDef PyAnimation::getsetters[] = {
{"property", (getter)get_property, NULL, "Target property name", NULL}, {"property", (getter)get_property, NULL, "Target property name", NULL},
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL}, {"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
@ -225,10 +239,23 @@ PyGetSetDef PyAnimation::getsetters[] = {
PyMethodDef PyAnimation::methods[] = { PyMethodDef PyAnimation::methods[] = {
{"start", (PyCFunction)start, METH_VARARGS, {"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", (PyCFunction)update, METH_VARARGS,
"Update the animation by deltaTime (returns True if still running)"}, "Update the animation by deltaTime (returns True if still running)"},
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
"Get the current interpolated value"}, "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} {NULL}
}; };

View File

@ -28,6 +28,8 @@ public:
static PyObject* start(PyAnimationObject* self, PyObject* args); static PyObject* start(PyAnimationObject* self, PyObject* args);
static PyObject* update(PyAnimationObject* self, PyObject* args); static PyObject* update(PyAnimationObject* self, PyObject* args);
static PyObject* get_current_value(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 PyGetSetDef getsetters[];
static PyMethodDef methods[]; static PyMethodDef methods[];