feat: implement mcrfpy.Timer object with pause/resume/cancel capabilities closes #103
- Created PyTimer.h/cpp with object-oriented timer interface - Enhanced PyTimerCallable with pause/resume state tracking - Added timer control methods: pause(), resume(), cancel(), restart() - Added timer properties: interval, remaining, paused, active, callback - Fixed timing logic to prevent rapid catch-up after resume - Timer objects automatically register with game engine - Added comprehensive test demonstrating all functionality
This commit is contained in:
parent
1aa35202e1
commit
27db9a4184
|
@ -164,6 +164,15 @@ void GameEngine::run()
|
|||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<PyTimerCallable> GameEngine::getTimer(const std::string& name)
|
||||
{
|
||||
auto it = timers.find(name);
|
||||
if (it != timers.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
||||
{
|
||||
auto it = timers.find(name);
|
||||
|
|
|
@ -29,12 +29,12 @@ class GameEngine
|
|||
bool headless = false;
|
||||
McRogueFaceConfig config;
|
||||
|
||||
sf::Clock runtime;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
void testTimers();
|
||||
|
||||
public:
|
||||
sf::Clock runtime;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
std::string scene;
|
||||
GameEngine();
|
||||
GameEngine(const McRogueFaceConfig& cfg);
|
||||
|
@ -54,6 +54,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);
|
||||
void setWindowScale(float);
|
||||
bool isHeadless() const { return headless; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "platform.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "PyDrawable.h"
|
||||
#include "PyTimer.h"
|
||||
#include "GameEngine.h"
|
||||
#include "UI.h"
|
||||
#include "Resources.h"
|
||||
|
@ -85,6 +86,9 @@ PyObject* PyInit_mcrfpy()
|
|||
|
||||
/*animation*/
|
||||
&PyAnimationType,
|
||||
|
||||
/*timer*/
|
||||
&PyTimerType,
|
||||
nullptr};
|
||||
int i = 0;
|
||||
auto t = pytypes[i];
|
||||
|
|
|
@ -16,21 +16,24 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
|
|||
return PyObject_Call(target, args, kwargs);
|
||||
}
|
||||
|
||||
bool PyCallable::isNone()
|
||||
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)
|
||||
: 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)
|
||||
: 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;
|
||||
}
|
||||
|
||||
|
@ -60,6 +63,62 @@ bool PyTimerCallable::test(int now)
|
|||
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)
|
||||
{}
|
||||
|
|
|
@ -10,7 +10,7 @@ protected:
|
|||
~PyCallable();
|
||||
PyObject* call(PyObject*, PyObject*);
|
||||
public:
|
||||
bool isNone();
|
||||
bool isNone() const;
|
||||
};
|
||||
|
||||
class PyTimerCallable: public PyCallable
|
||||
|
@ -19,11 +19,32 @@ 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);
|
||||
};
|
||||
|
||||
class PyClickCallable: public PyCallable
|
||||
|
|
|
@ -0,0 +1,271 @@
|
|||
#include "PyTimer.h"
|
||||
#include "PyCallable.h"
|
||||
#include "GameEngine.h"
|
||||
#include "Resources.h"
|
||||
#include <sstream>
|
||||
|
||||
PyObject* PyTimer::repr(PyObject* self) {
|
||||
PyTimerObject* timer = (PyTimerObject*)self;
|
||||
std::ostringstream oss;
|
||||
oss << "<Timer name='" << timer->name << "' ";
|
||||
|
||||
if (timer->data) {
|
||||
oss << "interval=" << timer->data->getInterval() << "ms ";
|
||||
oss << (timer->data->isPaused() ? "paused" : "active");
|
||||
} else {
|
||||
oss << "uninitialized";
|
||||
}
|
||||
oss << ">";
|
||||
|
||||
return PyUnicode_FromString(oss.str().c_str());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||
PyTimerObject* self = (PyTimerObject*)type->tp_alloc(type, 0);
|
||||
if (self) {
|
||||
new(&self->name) std::string(); // Placement new for std::string
|
||||
self->data = nullptr;
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
|
||||
static char* kwlist[] = {"name", "callback", "interval", NULL};
|
||||
const char* name = nullptr;
|
||||
PyObject* callback = nullptr;
|
||||
int interval = 0;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", kwlist,
|
||||
&name, &callback, &interval)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(callback)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (interval <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "interval must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->name = name;
|
||||
|
||||
// Get current time from game engine
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
// Create the timer callable
|
||||
self->data = std::make_shared<PyTimerCallable>(callback, interval, current_time);
|
||||
|
||||
// Register with game engine
|
||||
if (Resources::game) {
|
||||
Resources::game->timers[self->name] = self->data;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PyTimer::dealloc(PyTimerObject* 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly destroy std::string
|
||||
self->name.~basic_string();
|
||||
|
||||
// Clear shared_ptr
|
||||
self->data.reset();
|
||||
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
// Timer control methods
|
||||
PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->pause(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->resume(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Remove from game engine
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
self->data->cancel();
|
||||
self->data.reset();
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
self->data->restart(current_time);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Property getters/setters
|
||||
PyObject* PyTimer::get_interval(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->getInterval());
|
||||
}
|
||||
|
||||
int PyTimer::set_interval(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "interval must be an integer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long interval = PyLong_AsLong(value);
|
||||
if (interval <= 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "interval must be positive");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setInterval(interval);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_remaining(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int current_time = 0;
|
||||
if (Resources::game) {
|
||||
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
|
||||
}
|
||||
|
||||
return PyLong_FromLong(self->data->getRemaining(current_time));
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isPaused());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
return Py_False;
|
||||
}
|
||||
|
||||
return PyBool_FromLong(self->data->isActive());
|
||||
}
|
||||
|
||||
PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PyObject* callback = self->data->getCallback();
|
||||
if (!callback) {
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
Py_INCREF(callback);
|
||||
return callback;
|
||||
}
|
||||
|
||||
int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) {
|
||||
if (!self->data) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!PyCallable_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
self->data->setCallback(value);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyGetSetDef PyTimer::getsetters[] = {
|
||||
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
|
||||
"Timer interval in milliseconds", NULL},
|
||||
{"remaining", (getter)PyTimer::get_remaining, NULL,
|
||||
"Time remaining until next trigger in milliseconds", NULL},
|
||||
{"paused", (getter)PyTimer::get_paused, NULL,
|
||||
"Whether the timer is paused", NULL},
|
||||
{"active", (getter)PyTimer::get_active, NULL,
|
||||
"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},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyTimer::methods[] = {
|
||||
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
|
||||
"Pause the timer"},
|
||||
{"resume", (PyCFunction)PyTimer::resume, METH_NOARGS,
|
||||
"Resume a paused timer"},
|
||||
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
|
||||
"Cancel the timer and remove it from the system"},
|
||||
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
|
||||
"Restart the timer from the current time"},
|
||||
{NULL}
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class PyTimerCallable;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<PyTimerCallable> data;
|
||||
std::string name;
|
||||
} PyTimerObject;
|
||||
|
||||
class PyTimer
|
||||
{
|
||||
public:
|
||||
// Python type methods
|
||||
static PyObject* repr(PyObject* self);
|
||||
static int init(PyTimerObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
|
||||
static void dealloc(PyTimerObject* self);
|
||||
|
||||
// Timer control methods
|
||||
static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
|
||||
|
||||
// Timer property getters
|
||||
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);
|
||||
static PyObject* get_paused(PyTimerObject* self, void* closure);
|
||||
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 PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyTimerType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Timer",
|
||||
.tp_basicsize = sizeof(PyTimerObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyTimer::dealloc,
|
||||
.tp_repr = PyTimer::repr,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Timer object for scheduled callbacks"),
|
||||
.tp_methods = PyTimer::methods,
|
||||
.tp_getset = PyTimer::getsetters,
|
||||
.tp_init = (initproc)PyTimer::init,
|
||||
.tp_new = PyTimer::pynew,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue