timer fixes: timers managed by engine can run in the background. Closes #180

This commit is contained in:
John McCardle 2026-01-06 20:13:51 -05:00
commit b0b17f4633
4 changed files with 57 additions and 22 deletions

View file

@ -911,6 +911,32 @@ PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
return Py_None;
}
// #180: Create a Python wrapper for an existing C++ Timer (for orphaned timers)
static PyObject* createTimerWrapper(std::shared_ptr<Timer> timer)
{
// Allocate new PyTimerObject
PyTypeObject* type = &mcrfpydef::PyTimerType;
PyTimerObject* obj = (PyTimerObject*)type->tp_alloc(type, 0);
if (!obj) return nullptr;
// Initialize - use placement new for std::string
new(&obj->name) std::string(timer->name);
obj->data = timer; // Share the existing C++ Timer
obj->weakreflist = nullptr;
// Register in cache for future lookups
if (timer->serial_number == 0) {
timer->serial_number = PythonObjectCache::getInstance().assignSerial();
}
PyObject* weakref = PyWeakref_NewRef((PyObject*)obj, NULL);
if (weakref) {
PythonObjectCache::getInstance().registerObject(timer->serial_number, weakref);
Py_DECREF(weakref);
}
return (PyObject*)obj;
}
// #173: Get all timers as a tuple of Python Timer objects
PyObject* McRFPy_API::api_get_timers()
{
@ -918,23 +944,34 @@ PyObject* McRFPy_API::api_get_timers()
return PyTuple_New(0);
}
// Count timers that have Python wrappers
std::vector<PyObject*> timer_objs;
for (auto& pair : game->timers) {
auto& timer = pair.second;
if (timer && timer->serial_number != 0) {
PyObject* timer_obj = PythonObjectCache::getInstance().lookup(timer->serial_number);
if (!timer) continue;
PyObject* timer_obj = nullptr;
// Try to find existing Python wrapper
if (timer->serial_number != 0) {
timer_obj = PythonObjectCache::getInstance().lookup(timer->serial_number);
}
// #180: If no wrapper exists (orphaned timer), create a new one
if (!timer_obj || timer_obj == Py_None) {
timer_obj = createTimerWrapper(timer);
}
if (timer_obj && timer_obj != Py_None) {
timer_objs.push_back(timer_obj);
}
}
}
PyObject* tuple = PyTuple_New(timer_objs.size());
if (!tuple) return NULL;
for (Py_ssize_t i = 0; i < static_cast<Py_ssize_t>(timer_objs.size()); i++) {
Py_INCREF(timer_objs[i]);
// timer_objs already has a reference from lookup() or createTimerWrapper()
// PyTuple_SET_ITEM steals that reference, so no additional INCREF needed
PyTuple_SET_ITEM(tuple, i, timer_objs[i]);
}

View file

@ -80,8 +80,8 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
// Create the timer with start parameter
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once, (bool)start);
// Create the timer with start parameter and name (#180)
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once, (bool)start, self->name);
// Register in Python object cache
if (self->data->serial_number == 0) {
@ -112,18 +112,14 @@ void PyTimer::dealloc(PyTimerObject* self) {
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);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
// #180: DON'T remove from game->timers - timer keeps running even without Python wrapper.
// The engine's shared_ptr keeps the Timer alive. When timer.stop() is called or
// a one-shot timer fires, testTimers() removes it from the map.
// Explicitly destroy std::string
self->name.~basic_string();
// Clear shared_ptr - this is the only place that truly destroys the Timer
// Clear our shared_ptr - if engine still has one, Timer lives on
self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self);

View file

@ -4,9 +4,9 @@
#include "McRFPy_API.h"
#include "GameEngine.h"
Timer::Timer(PyObject* _target, int _interval, int now, bool _once, bool _start)
Timer::Timer(PyObject* _target, int _interval, int now, bool _once, bool _start, const std::string& _name)
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0), once(_once), stopped(!_start)
paused(false), pause_start_time(0), total_paused_time(0), once(_once), stopped(!_start), name(_name)
{}
Timer::Timer()

View file

@ -2,6 +2,7 @@
#include "Common.h"
#include "Python.h"
#include <memory>
#include <string>
class PyCallable;
class GameEngine; // forward declare
@ -26,9 +27,10 @@ private:
public:
uint64_t serial_number = 0; // For Python object cache
std::string name; // Store name for creating Python wrappers (#180)
Timer(); // for map to build
Timer(PyObject* target, int interval, int now, bool once = false, bool start = true);
Timer(PyObject* target, int interval, int now, bool once = false, bool start = true, const std::string& name = "");
~Timer();
// Core timer functionality