From be6deae46ffe232dab643a2b0f7712a7d5c7dd4e Mon Sep 17 00:00:00 2001 From: Michael Theall Date: Thu, 2 Nov 2023 20:36:31 -0500 Subject: [PATCH] Add game event support --- python-mtheall/Arena.cpp | 622 +++++++++++++++++++++++++++++++----- python-mtheall/Car.cpp | 56 +++- python-mtheall/CarState.cpp | 13 +- python-mtheall/Module.h | 66 ++-- python-mtheall/unit_test.py | 155 ++++++++- src/Sim/Arena/Arena.cpp | 2 +- src/Sim/GameMode.h | 5 +- 7 files changed, 799 insertions(+), 120 deletions(-) diff --git a/python-mtheall/Arena.cpp b/python-mtheall/Arena.cpp index a39478e3..261c7d9c 100644 --- a/python-mtheall/Arena.cpp +++ b/python-mtheall/Arena.cpp @@ -380,6 +380,13 @@ class Arena::ThreadPool } } + // update game tracker events + for (auto &arena : arenas_) + { + if (arena->gameEvent) + arena->gameEvent->Update (arena->arena.get ()); + } + return true; } @@ -583,6 +590,27 @@ Returns previous (callback, data))"}, .ml_doc = R"(set_goal_score_callback(self, callback, data = None) -> tuple `callback` must be callable e.g. `def callback(arena: RocketSim.Arena, scoring_team: int, data)` `callback` always called with keyword arguments +Returns previous (callback, data))"}, + {.ml_name = "set_shot_event_callback", + .ml_meth = (PyCFunction)&Arena::SetShotEventCallback, + .ml_flags = METH_VARARGS | METH_KEYWORDS, + .ml_doc = R"(set_shot_event_callback(self, callback, data = None) -> tuple +`callback` must be callable e.g. `def callback(arena: RocketSim.Arena, shooter: RocketSim.Car, passer: RocketSim.Car, data)` +`callback` always called with keyword arguments +Returns previous (callback, data))"}, + {.ml_name = "set_goal_event_callback", + .ml_meth = (PyCFunction)&Arena::SetGoalEventCallback, + .ml_flags = METH_VARARGS | METH_KEYWORDS, + .ml_doc = R"(set_goal_event_callback(self, callback, data = None) -> tuple +`callback` must be callable e.g. `def callback(arena: RocketSim.Arena, scorer: RocketSim.Car, passer: RocketSim.Car, data)` +`callback` always called with keyword arguments +Returns previous (callback, data))"}, + {.ml_name = "set_save_event_callback", + .ml_meth = (PyCFunction)&Arena::SetSaveEventCallback, + .ml_flags = METH_VARARGS | METH_KEYWORDS, + .ml_doc = R"(set_save_event_callback(self, callback, data = None) -> tuple +`callback` must be callable e.g. `def callback(arena: RocketSim.Arena, saver: RocketSim.Car, data)` +`callback` always called with keyword arguments Returns previous (callback, data))"}, {.ml_name = "set_mutator_config", .ml_meth = (PyCFunction)&Arena::SetMutatorConfig, @@ -655,6 +683,7 @@ PyObject *Arena::New (PyTypeObject *subtype_, PyObject *args_, PyObject *kwds_) self->boostPads = new (std::nothrow) std::unordered_map<::BoostPad *, PyRef>{}; self->boostPadsByIndex = nullptr; self->ballPrediction = nullptr; + self->gameEvent = nullptr; self->ball = nullptr; self->ballTouchCallback = nullptr; self->ballTouchCallbackUserData = nullptr; @@ -666,6 +695,12 @@ PyObject *Arena::New (PyTypeObject *subtype_, PyObject *args_, PyObject *kwds_) self->carDemoCallbackUserData = nullptr; self->goalScoreCallback = nullptr; self->goalScoreCallbackUserData = nullptr; + self->shotEventCallback = nullptr; + self->shotEventCallbackUserData = nullptr; + self->goalEventCallback = nullptr; + self->goalEventCallbackUserData = nullptr; + self->saveEventCallback = nullptr; + self->saveEventCallbackUserData = nullptr; self->blueScore = 0; self->orangeScore = 0; self->lastGoalTick = 0; @@ -702,17 +737,31 @@ int Arena::Init (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept if (!PyArg_ParseTupleAndKeywords (args_, kwds_, "|iif", dict, &gameMode, &memoryWeightMode, &tickRate)) return -1; - switch (static_cast<::GameMode> (gameMode)) + try { - case ::GameMode::SOCCAR: - case ::GameMode::HOOPS: - case ::GameMode::HEATSEEKER: - case ::GameMode::SNOWDAY: - case ::GameMode::THE_VOID: - break; + switch (static_cast<::GameMode> (gameMode)) + { + case ::GameMode::SOCCAR: + case ::GameMode::HOOPS: + case ::GameMode::SNOWDAY: + self_->gameEvent = new ::GameEventTracker{}; + self_->gameEvent->SetShotCallback (&Arena::HandleShotEventCallback, self_); + self_->gameEvent->SetGoalCallback (&Arena::HandleGoalEventCallback, self_); + self_->gameEvent->SetSaveCallback (&Arena::HandleSaveEventCallback, self_); + break; - default: - PyErr_SetString (PyExc_RuntimeError, "Invalid game mode"); + case ::GameMode::HEATSEEKER: + case ::GameMode::THE_VOID: + break; + + default: + PyErr_SetString (PyExc_RuntimeError, "Invalid game mode"); + return -1; + } + } + catch (...) + { + PyErr_NoMemory (); return -1; } @@ -782,6 +831,12 @@ int Arena::Init (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept PyObjectRef::assign (self_->carDemoCallbackUserData, Py_None); PyObjectRef::assign (self_->goalScoreCallback, Py_None); PyObjectRef::assign (self_->goalScoreCallbackUserData, Py_None); + PyObjectRef::assign (self_->shotEventCallback, Py_None); + PyObjectRef::assign (self_->shotEventCallbackUserData, Py_None); + PyObjectRef::assign (self_->goalEventCallback, Py_None); + PyObjectRef::assign (self_->goalEventCallbackUserData, Py_None); + PyObjectRef::assign (self_->saveEventCallback, Py_None); + PyObjectRef::assign (self_->saveEventCallbackUserData, Py_None); return 0; } @@ -800,6 +855,7 @@ void Arena::Dealloc (Arena *self_) noexcept delete self_->boostPads; delete self_->boostPadsByIndex; delete self_->ballPrediction; + delete self_->gameEvent; Py_XDECREF (self_->ball); Py_XDECREF (self_->ballTouchCallback); Py_XDECREF (self_->ballTouchCallbackUserData); @@ -811,6 +867,12 @@ void Arena::Dealloc (Arena *self_) noexcept Py_XDECREF (self_->carDemoCallbackUserData); Py_XDECREF (self_->goalScoreCallback); Py_XDECREF (self_->goalScoreCallbackUserData); + Py_XDECREF (self_->shotEventCallback); + Py_XDECREF (self_->shotEventCallbackUserData); + Py_XDECREF (self_->goalEventCallback); + Py_XDECREF (self_->goalEventCallbackUserData); + Py_XDECREF (self_->saveEventCallback); + Py_XDECREF (self_->saveEventCallbackUserData); auto const tp_free = (freefunc)PyType_GetSlot (Type, Py_tp_free); tp_free (self_); @@ -960,6 +1022,30 @@ PyObject *Arena::Pickle (Arena *self_) noexcept !DictSetValue (dict.borrow (), "goal_score_callback_user_data", PyNewRef (self_->goalScoreCallbackUserData))) return nullptr; + if (self_->shotEventCallback != Py_None && + !DictSetValue (dict.borrow (), "shot_event_callback", PyNewRef (self_->shotEventCallback))) + return nullptr; + + if (self_->shotEventCallbackUserData != Py_None && + !DictSetValue (dict.borrow (), "shot_event_callback_user_data", PyNewRef (self_->shotEventCallbackUserData))) + return nullptr; + + if (self_->goalEventCallback != Py_None && + !DictSetValue (dict.borrow (), "goal_event_callback", PyNewRef (self_->goalEventCallback))) + return nullptr; + + if (self_->goalEventCallbackUserData != Py_None && + !DictSetValue (dict.borrow (), "goal_event_callback_user_data", PyNewRef (self_->goalEventCallbackUserData))) + return nullptr; + + if (self_->saveEventCallback != Py_None && + !DictSetValue (dict.borrow (), "save_event_callback", PyNewRef (self_->saveEventCallback))) + return nullptr; + + if (self_->saveEventCallbackUserData != Py_None && + !DictSetValue (dict.borrow (), "save_event_callback_user_data", PyNewRef (self_->saveEventCallbackUserData))) + return nullptr; + return dict.gift (); } @@ -998,6 +1084,12 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept static char carDemoCallbackUserDataKwd[] = "car_demo_callback_user_data"; static char goalScoreCallbackKwd[] = "goal_score_callback"; static char goalScoreCallbackUserDataKwd[] = "goal_score_callback_user_data"; + static char shotEventCallbackKwd[] = "shot_score_callback"; + static char shotEventCallbackUserDataKwd[] = "shot_score_callback_user_data"; + static char goalEventCallbackKwd[] = "goal_score_callback"; + static char goalEventCallbackUserDataKwd[] = "goal_score_callback_user_data"; + static char saveEventCallbackKwd[] = "save_score_callback"; + static char saveEventCallbackUserDataKwd[] = "save_score_callback_user_data"; static char *dict[] = {gameModeKwd, memoryWeightModeKwd, @@ -1022,6 +1114,12 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept carDemoCallbackUserDataKwd, goalScoreCallbackKwd, goalScoreCallbackUserDataKwd, + shotEventCallbackKwd, + shotEventCallbackUserDataKwd, + goalEventCallbackKwd, + goalEventCallbackUserDataKwd, + saveEventCallbackKwd, + saveEventCallbackUserDataKwd, nullptr}; PyObject *mutatorConfig = nullptr; // borrowed references @@ -1038,6 +1136,12 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept PyObject *carDemoCallbackUserData = nullptr; PyObject *goalScoreCallback = nullptr; PyObject *goalScoreCallbackUserData = nullptr; + PyObject *shotEventCallback = nullptr; + PyObject *shotEventCallbackUserData = nullptr; + PyObject *goalEventCallback = nullptr; + PyObject *goalEventCallbackUserData = nullptr; + PyObject *saveEventCallback = nullptr; + PyObject *saveEventCallbackUserData = nullptr; unsigned long long tickCount = 0; unsigned long long lastGoalTick = 0; unsigned long long lastGymStateTick = 0; @@ -1049,7 +1153,7 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept int memoryWeightMode = static_cast (::ArenaMemWeightMode::HEAVY); if (!PyArg_ParseTupleAndKeywords (dummy.borrow (), dict_, - "|iikO!fKO!O!O!IIKKOOOOOOOO", + "|iikO!fKO!O!O!IIKKOOOOOOOOOOOOOO", dict, &gameMode, &memoryWeightMode, @@ -1077,7 +1181,13 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept &carDemoCallback, &carDemoCallbackUserData, &goalScoreCallback, - &goalScoreCallbackUserData)) + &goalScoreCallbackUserData, + &shotEventCallback, + &shotEventCallbackUserData, + &goalEventCallback, + &goalEventCallbackUserData, + &saveEventCallback, + &saveEventCallbackUserData)) return nullptr; switch (static_cast<::GameMode> (gameMode)) @@ -1128,6 +1238,24 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept return nullptr; } + if (shotEventCallback && shotEventCallback != Py_None && !PyCallable_Check (shotEventCallback)) + { + PyErr_SetString (PyExc_ValueError, "Invalid shot event callback"); + return nullptr; + } + + if (goalEventCallback && goalEventCallback != Py_None && !PyCallable_Check (goalEventCallback)) + { + PyErr_SetString (PyExc_ValueError, "Invalid goal event callback"); + return nullptr; + } + + if (saveEventCallback && saveEventCallback != Py_None && !PyCallable_Check (saveEventCallback)) + { + PyErr_SetString (PyExc_ValueError, "Invalid save event callback"); + return nullptr; + } + try { // default initialization if it hasn't been done yet @@ -1265,6 +1393,12 @@ PyObject *Arena::Unpickle (Arena *self_, PyObject *dict_) noexcept PyObjectRef::assign (self_->carDemoCallbackUserData, carDemoCallbackUserData); PyObjectRef::assign (self_->goalScoreCallback, goalScoreCallback); PyObjectRef::assign (self_->goalScoreCallbackUserData, goalScoreCallbackUserData); + PyObjectRef::assign (self_->shotEventCallback, shotEventCallback); + PyObjectRef::assign (self_->shotEventCallbackUserData, shotEventCallbackUserData); + PyObjectRef::assign (self_->goalEventCallback, goalEventCallback); + PyObjectRef::assign (self_->goalEventCallbackUserData, goalEventCallbackUserData); + PyObjectRef::assign (self_->saveEventCallback, saveEventCallback); + PyObjectRef::assign (self_->saveEventCallbackUserData, saveEventCallbackUserData); if (self_->ballTouchCallback != Py_None) self_->arena->SetBallTouchCallback (&Arena::HandleBallTouchCallback, self_); @@ -1423,12 +1557,18 @@ PyObject *Arena::Clone (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept carRef->goals = it->second->goals; carRef->demos = it->second->demos; carRef->boostPickups = it->second->boostPickups; + carRef->shots = it->second->shots; + carRef->saves = it->second->saves; + carRef->assists = it->second->assists; } else { carRef->goals = 0; carRef->demos = 0; carRef->boostPickups = 0; + carRef->shots = 0; + carRef->saves = 0; + carRef->assists = 0; } } @@ -1454,6 +1594,12 @@ PyObject *Arena::Clone (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept PyObjectRef::assign (clone->carDemoCallbackUserData, self_->carDemoCallbackUserData); PyObjectRef::assign (clone->goalScoreCallback, self_->goalScoreCallback); PyObjectRef::assign (clone->goalScoreCallbackUserData, self_->goalScoreCallbackUserData); + PyObjectRef::assign (clone->shotEventCallback, self_->shotEventCallback); + PyObjectRef::assign (clone->shotEventCallbackUserData, self_->shotEventCallbackUserData); + PyObjectRef::assign (clone->goalEventCallback, self_->goalEventCallback); + PyObjectRef::assign (clone->goalEventCallbackUserData, self_->goalEventCallbackUserData); + PyObjectRef::assign (clone->saveEventCallback, self_->saveEventCallback); + PyObjectRef::assign (clone->saveEventCallbackUserData, self_->saveEventCallbackUserData); } else { @@ -1467,6 +1613,12 @@ PyObject *Arena::Clone (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept PyObjectRef::assign (clone->carDemoCallbackUserData, Py_None); PyObjectRef::assign (clone->goalScoreCallback, Py_None); PyObjectRef::assign (clone->goalScoreCallbackUserData, Py_None); + PyObjectRef::assign (clone->shotEventCallback, Py_None); + PyObjectRef::assign (clone->shotEventCallbackUserData, Py_None); + PyObjectRef::assign (clone->goalEventCallback, Py_None); + PyObjectRef::assign (clone->goalEventCallbackUserData, Py_None); + PyObjectRef::assign (clone->saveEventCallback, Py_None); + PyObjectRef::assign (clone->saveEventCallbackUserData, Py_None); } if (clone->ballTouchCallback != Py_None) @@ -1592,6 +1744,9 @@ PyObject *Arena::CloneInto (Arena *self_, PyObject *args_, PyObject *kwds_) noex carB->goals = carA->goals; carB->demos = carA->demos; carB->boostPickups = carA->boostPickups; + carB->shots = carA->shots; + carB->saves = carA->saves; + carB->assists = carA->assists; } if (copyCallbacks) @@ -1606,6 +1761,12 @@ PyObject *Arena::CloneInto (Arena *self_, PyObject *args_, PyObject *kwds_) noex PyObjectRef::assign (target->carDemoCallbackUserData, self_->carDemoCallbackUserData); PyObjectRef::assign (target->goalScoreCallback, self_->goalScoreCallback); PyObjectRef::assign (target->goalScoreCallbackUserData, self_->goalScoreCallbackUserData); + PyObjectRef::assign (target->shotEventCallback, self_->shotEventCallback); + PyObjectRef::assign (target->shotEventCallbackUserData, self_->shotEventCallbackUserData); + PyObjectRef::assign (target->goalEventCallback, self_->goalEventCallback); + PyObjectRef::assign (target->goalEventCallbackUserData, self_->goalEventCallbackUserData); + PyObjectRef::assign (target->saveEventCallback, self_->saveEventCallback); + PyObjectRef::assign (target->saveEventCallbackUserData, self_->saveEventCallbackUserData); } else { @@ -1619,6 +1780,12 @@ PyObject *Arena::CloneInto (Arena *self_, PyObject *args_, PyObject *kwds_) noex PyObjectRef::assign (target->carDemoCallbackUserData, Py_None); PyObjectRef::assign (target->goalScoreCallback, Py_None); PyObjectRef::assign (target->goalScoreCallbackUserData, Py_None); + PyObjectRef::assign (target->shotEventCallback, Py_None); + PyObjectRef::assign (target->shotEventCallbackUserData, Py_None); + PyObjectRef::assign (target->goalEventCallback, Py_None); + PyObjectRef::assign (target->goalEventCallbackUserData, Py_None); + PyObjectRef::assign (target->saveEventCallback, Py_None); + PyObjectRef::assign (target->saveEventCallbackUserData, Py_None); } if (target->ballTouchCallback == Py_None) @@ -1885,8 +2052,8 @@ DID YOU STATE SET MULTIPLE OBJECTS IN THE SAME LOCATION?)"); carState (i, 0) = car->car->id; carState (i, 1) = static_cast (car->car->team); carState (i, 2) = car->goals; - carState (i, 3) = 0; // todo saves - carState (i, 4) = 0; // todo shots + carState (i, 3) = car->saves; + carState (i, 4) = car->shots; carState (i, 5) = car->demos; carState (i, 6) = car->boostPickups; carState (i, 7) = state.isDemoed; @@ -2181,6 +2348,114 @@ PyObject *Arena::SetGoalScoreCallback (Arena *self_, PyObject *args_, PyObject * return prev.giftObject (); } +PyObject *Arena::SetShotEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept +{ + if (!self_->gameEvent) + { + PyErr_Format (PyExc_RuntimeError, + "Cannot set a shot event callback when on %s gamemode!", + GAMEMODE_STRS[static_cast (self_->arena->gameMode)]); + return nullptr; + } + + static char callbackKwd[] = "callback"; + static char dataKwd[] = "data"; + + static char *dict[] = {callbackKwd, dataKwd, nullptr}; + + PyObject *callback; // borrowed references + PyObject *userData = Py_None; + if (!PyArg_ParseTupleAndKeywords (args_, kwds_, "O|O", dict, &callback, &userData)) + return nullptr; + + if (callback != Py_None && !PyCallable_Check (callback)) + { + PyErr_SetString (PyExc_RuntimeError, "First parameter must be a callable object or None"); + return nullptr; + } + + auto prev = PyObjectRef::steal (PyTuple_Pack (2, callback, userData)); + if (!prev) + return nullptr; + + PyObjectRef::assign (self_->shotEventCallback, PyTuple_GetItem (prev.borrow (), 0)); + PyObjectRef::assign (self_->shotEventCallbackUserData, PyTuple_GetItem (prev.borrow (), 1)); + + return prev.giftObject (); +} + +PyObject *Arena::SetGoalEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept +{ + if (!self_->gameEvent) + { + PyErr_Format (PyExc_RuntimeError, + "Cannot set a goal event callback when on %s gamemode!", + GAMEMODE_STRS[static_cast (self_->arena->gameMode)]); + return nullptr; + } + + static char callbackKwd[] = "callback"; + static char dataKwd[] = "data"; + + static char *dict[] = {callbackKwd, dataKwd, nullptr}; + + PyObject *callback; // borrowed references + PyObject *userData = Py_None; + if (!PyArg_ParseTupleAndKeywords (args_, kwds_, "O|O", dict, &callback, &userData)) + return nullptr; + + if (callback != Py_None && !PyCallable_Check (callback)) + { + PyErr_SetString (PyExc_RuntimeError, "First parameter must be a callable object or None"); + return nullptr; + } + + auto prev = PyObjectRef::steal (PyTuple_Pack (2, callback, userData)); + if (!prev) + return nullptr; + + PyObjectRef::assign (self_->goalEventCallback, PyTuple_GetItem (prev.borrow (), 0)); + PyObjectRef::assign (self_->goalEventCallbackUserData, PyTuple_GetItem (prev.borrow (), 1)); + + return prev.giftObject (); +} + +PyObject *Arena::SetSaveEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept +{ + if (!self_->gameEvent) + { + PyErr_Format (PyExc_RuntimeError, + "Cannot set a save event callback when on %s gamemode!", + GAMEMODE_STRS[static_cast (self_->arena->gameMode)]); + return nullptr; + } + + static char callbackKwd[] = "callback"; + static char dataKwd[] = "data"; + + static char *dict[] = {callbackKwd, dataKwd, nullptr}; + + PyObject *callback; // borrowed references + PyObject *userData = Py_None; + if (!PyArg_ParseTupleAndKeywords (args_, kwds_, "O|O", dict, &callback, &userData)) + return nullptr; + + if (callback != Py_None && !PyCallable_Check (callback)) + { + PyErr_SetString (PyExc_RuntimeError, "First parameter must be a callable object or None"); + return nullptr; + } + + auto prev = PyObjectRef::steal (PyTuple_Pack (2, callback, userData)); + if (!prev) + return nullptr; + + PyObjectRef::assign (self_->saveEventCallback, PyTuple_GetItem (prev.borrow (), 0)); + PyObjectRef::assign (self_->saveEventCallbackUserData, PyTuple_GetItem (prev.borrow (), 1)); + + return prev.giftObject (); +} + PyObject *Arena::SetMutatorConfig (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept { static char configKwd[] = "config"; @@ -2208,12 +2483,16 @@ PyObject *Arena::Step (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept Py_BEGIN_ALLOW_THREADS; self_->arena->Step (ticksToSimulate); + Py_END_ALLOW_THREADS; + + if (self_->gameEvent) + self_->gameEvent->Update (self_->arena.get ()); + if (self_->stepExceptionType) { restoreException (self_); return nullptr; } - Py_END_ALLOW_THREADS; Py_RETURN_NONE; } @@ -2317,17 +2596,21 @@ void Arena::HandleBallTouchCallback (::Arena *arena_, ::Car *car_, void *userDat if (self->ballTouchCallback == Py_None) return; - auto it = self->cars->find (car_->id); - if (it == std::end (*self->cars) || !it->second) + auto car = PyRef::incObjectRef (Py_None); + if (car_) { - auto const gil = GIL{}; + auto it = self->cars->find (car_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; - PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", car_->id); - saveException (self); - return; - } + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", car_->id); + saveException (self); + return; + } - auto const car = it->second; + car = it->second; + } auto const gil = GIL{}; @@ -2359,36 +2642,44 @@ void Arena::HandleBoostPickupCallback (::Arena *arena_, ::Car *car_, ::BoostPad if (self->stepExceptionType) return; - auto it = self->cars->find (car_->id); - if (it == std::end (*self->cars) || !it->second) + auto car = PyRef::incObjectRef (Py_None); + if (car_) { - // this should never happen - auto const gil = GIL{}; + auto const it = self->cars->find (car_->id); + if (it == std::end (*self->cars) || !it->second) + { + // this should never happen + auto const gil = GIL{}; - PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", car_->id); - saveException (self); - return; + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", car_->id); + saveException (self); + return; + } + + car = it->second; + ++car->boostPickups; } - auto it2 = self->boostPads->find (boostPad_); - if (it2 == std::end (*self->boostPads) || !it->second) + auto boostPad = PyRef::incObjectRef (Py_None); + if (boostPad_) { - // this should never happen - auto const gil = GIL{}; + auto const it = self->boostPads->find (boostPad_); + if (it == std::end (*self->boostPads) || !it->second) + { + // this should never happen + auto const gil = GIL{}; - PyErr_SetString (PyExc_KeyError, "Boost pad not found"); - saveException (self); - return; - } + PyErr_SetString (PyExc_KeyError, "Boost pad not found"); + saveException (self); + return; + } - auto car = it->second; - ++car->boostPickups; + boostPad = it->second; + } if (self->boostPickupCallback == Py_None) return; - auto boostPad = it2->second; - auto const gil = GIL{}; auto args = PyObjectRef::steal (PyTuple_New (0)); @@ -2430,36 +2721,44 @@ void Arena::HandleCarBumpCallback (::Arena *arena_, if (self->stepExceptionType) return; - auto it = self->cars->find (bumper_->id); - if (it == std::end (*self->cars) || !it->second) + auto bumper = PyRef::incObjectRef (Py_None); + if (bumper_) { - auto const gil = GIL{}; + auto const it = self->cars->find (bumper_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; - PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", bumper_->id); - saveException (self); - return; - } + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", bumper_->id); + saveException (self); + return; + } - auto bumper = it->second; - if (isDemo_) - ++bumper->demos; + bumper = it->second; + if (isDemo_) + ++bumper->demos; + } if (self->carBumpCallback == Py_None && !isDemo_) return; - it = self->cars->find (victim_->id); - if (it == std::end (*self->cars) || !it->second) + auto victim = PyRef::incObjectRef (Py_None); + if (victim_) { - auto const gil = GIL{}; + auto const it = self->cars->find (victim_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; - PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", victim_->id); - saveException (self); - return; - } + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", victim_->id); + saveException (self); + return; + } - auto victim = it->second; - if (isDemo_) - victim->demoState = victim->car->GetState (); + victim = it->second; + if (isDemo_) + victim->demoState = victim->car->GetState (); + } if (self->carBumpCallback == Py_None && (!isDemo_ || self->carDemoCallback == Py_None)) return; @@ -2544,25 +2843,6 @@ void Arena::HandleGoalScoreCallback (::Arena *arena_, ::Team scoringTeam_, void ++self->blueScore; else ++self->orangeScore; - - // find which car scored - Car *best = nullptr; - for (auto const &[id, car] : *self->cars) - { - if (scoringTeam_ != car->car->team) - continue; - - if (!car->car->_internalState.ballHitInfo.isValid || - car->car->_internalState.ballHitInfo.tickCountWhenHit < self->lastGoalTick) - continue; - - if (!best || best->car->_internalState.ballHitInfo.tickCountWhenHit < - car->car->_internalState.ballHitInfo.tickCountWhenHit) - best = car.borrow (); - } - - if (best) - ++best->goals; } self->lastGoalTick = self->arena->tickCount; @@ -2596,6 +2876,188 @@ void Arena::HandleGoalScoreCallback (::Arena *arena_, ::Team scoringTeam_, void } } +void Arena::HandleShotEventCallback (::Arena *arena_, ::Car *shooter_, ::Car *passer_, void *userData_) noexcept +{ + auto const self = reinterpret_cast (userData_); + if (self->stepExceptionType) + return; + + auto shooter = PyRef::incObjectRef (Py_None); + if (shooter_) + { + auto const it = self->cars->find (shooter_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; + + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", shooter_->id); + saveException (self); + return; + } + + shooter = it->second; + ++shooter->shots; + } + + if (self->shotEventCallback == Py_None) + return; + + auto passer = PyRef::incObjectRef (Py_None); + if (passer_) + { + auto const it = self->cars->find (passer_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; + + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", passer_->id); + saveException (self); + return; + } + + passer = it->second; + } + + auto const gil = GIL{}; + + auto args = PyObjectRef::steal (PyTuple_New (0)); + if (!args) + { + saveException (self); + return; + } + + auto kwds = PyObjectRef::steal (Py_BuildValue ("{sOsOsOsO}", + "arena", + self, + "shooter", + shooter.borrow (), + "passer", + passer.borrow (), + "data", + self->shotEventCallbackUserData)); + + if (!PyObject_Call (self->shotEventCallback, args.borrow (), kwds.borrow ())) + { + saveException (self); + return; + } +} + +void Arena::HandleGoalEventCallback (::Arena *arena_, ::Car *shooter_, ::Car *passer_, void *userData_) noexcept +{ + auto const self = reinterpret_cast (userData_); + if (self->stepExceptionType) + return; + + auto shooter = PyRef::incObjectRef (Py_None); + if (shooter_) + { + auto const it = self->cars->find (shooter_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; + + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", shooter_->id); + saveException (self); + return; + } + + shooter = it->second; + ++shooter->goals; + } + + auto passer = PyRef::incObjectRef (Py_None); + if (passer_) + { + auto const it = self->cars->find (passer_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; + + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", passer_->id); + saveException (self); + return; + } + + passer = it->second; + ++passer->assists; + } + + if (self->goalEventCallback == Py_None) + return; + + auto const gil = GIL{}; + + auto args = PyObjectRef::steal (PyTuple_New (0)); + if (!args) + { + saveException (self); + return; + } + + auto kwds = PyObjectRef::steal (Py_BuildValue ("{sOsOsOsO}", + "arena", + self, + "shooter", + shooter.borrow (), + "passer", + passer.borrow (), + "data", + self->goalEventCallbackUserData)); + + if (!PyObject_Call (self->goalEventCallback, args.borrow (), kwds.borrow ())) + { + saveException (self); + return; + } +} + +void Arena::HandleSaveEventCallback (::Arena *arena_, ::Car *saver_, void *userData_) noexcept +{ + auto const self = reinterpret_cast (userData_); + if (self->stepExceptionType) + return; + + auto saver = PyRef::incObjectRef (Py_None); + if (saver_) + { + auto const it = self->cars->find (saver_->id); + if (it == std::end (*self->cars) || !it->second) + { + auto const gil = GIL{}; + + PyErr_Format (PyExc_KeyError, "Car with id '%" PRIu32 "' not found", saver_->id); + saveException (self); + return; + } + + saver = it->second; + ++saver->saves; + } + + if (self->saveEventCallback == Py_None) + return; + + auto const gil = GIL{}; + + auto args = PyObjectRef::steal (PyTuple_New (0)); + if (!args) + { + saveException (self); + return; + } + + auto kwds = PyObjectRef::steal ( + Py_BuildValue ("{sOsOsO}", "arena", self, "saver", saver.borrow (), "data", self->saveEventCallbackUserData)); + + if (!PyObject_Call (self->saveEventCallback, args.borrow (), kwds.borrow ())) + { + saveException (self); + return; + } +} + PyObject *Arena::Getgame_mode (Arena *self_, void *) noexcept { return PyLong_FromLong (static_cast (self_->arena->gameMode)); diff --git a/python-mtheall/Car.cpp b/python-mtheall/Car.cpp index 7e51f321..405e87c0 100644 --- a/python-mtheall/Car.cpp +++ b/python-mtheall/Car.cpp @@ -20,6 +20,21 @@ PyMemberDef Car::Members[] = { .offset = offsetof (Car, goals), .flags = 0, .doc = "Goals"}, + {.name = "shots", + .type = TypeHelper::type, + .offset = offsetof (Car, shots), + .flags = 0, + .doc = "Shots"}, + {.name = "saves", + .type = TypeHelper::type, + .offset = offsetof (Car, saves), + .flags = 0, + .doc = "Saves"}, + {.name = "assists", + .type = TypeHelper::type, + .offset = offsetof (Car, assists), + .flags = 0, + .doc = "Assists"}, {.name = nullptr, .type = 0, .offset = 0, .flags = 0, .doc = nullptr}, }; @@ -161,6 +176,15 @@ PyObject *Car::InternalPickle (Car *self_) noexcept !DictSetValue (dict.borrow (), "boost_pickups", PyLong_FromUnsignedLong (self_->boostPickups))) return nullptr; + if (self_->shots && !DictSetValue (dict.borrow (), "shots", PyLong_FromUnsignedLong (self_->shots))) + return nullptr; + + if (self_->saves && !DictSetValue (dict.borrow (), "saves", PyLong_FromUnsignedLong (self_->saves))) + return nullptr; + + if (self_->assists && !DictSetValue (dict.borrow (), "assists", PyLong_FromUnsignedLong (self_->assists))) + return nullptr; + return dict.gift (); } @@ -178,9 +202,22 @@ PyObject *Car::InternalUnpickle (std::shared_ptr<::Arena> arena_, Car *self_, Py static char goalsKwd[] = "goals"; static char demosKwd[] = "demos"; static char boostPickupsKwd[] = "boost_pickups"; - - static char *dict[] = { - idKwd, teamKwd, stateKwd, configKwd, controlsKwd, goalsKwd, demosKwd, boostPickupsKwd, nullptr}; + static char shotsKwd[] = "shots"; + static char savesKwd[] = "saves"; + static char assistsKwd[] = "assists"; + + static char *dict[] = {idKwd, + teamKwd, + stateKwd, + configKwd, + controlsKwd, + goalsKwd, + demosKwd, + boostPickupsKwd, + shotsKwd, + savesKwd, + assistsKwd, + nullptr}; PyObject *state = nullptr; // borrowed references PyObject *config = nullptr; @@ -189,10 +226,13 @@ PyObject *Car::InternalUnpickle (std::shared_ptr<::Arena> arena_, Car *self_, Py unsigned goals = 0; unsigned demos = 0; unsigned boostPickups = 0; + unsigned shots = 0; + unsigned saves = 0; + unsigned assists = 0; int team = static_cast (::Team::BLUE); if (!PyArg_ParseTupleAndKeywords (dummy.borrow (), dict_, - "|kiO!O!O!III", + "|kiO!O!O!IIIIII", dict, &id, &team, @@ -204,7 +244,10 @@ PyObject *Car::InternalUnpickle (std::shared_ptr<::Arena> arena_, Car *self_, Py &controls, &goals, &demos, - &boostPickups)) + &boostPickups, + &shots, + &saves, + &assists)) return nullptr; if (id == 0) @@ -242,6 +285,9 @@ PyObject *Car::InternalUnpickle (std::shared_ptr<::Arena> arena_, Car *self_, Py self_->goals = goals; self_->demos = demos; self_->boostPickups = boostPickups; + self_->shots = shots; + self_->saves = saves; + self_->assists = assists; self_->car->SetState (CarState::ToCarState (PyCast (state))); self_->car->_internalState.updateCounter = PyCast (state)->state.updateCounter; diff --git a/python-mtheall/CarState.cpp b/python-mtheall/CarState.cpp index a3717a78..875d895f 100644 --- a/python-mtheall/CarState.cpp +++ b/python-mtheall/CarState.cpp @@ -593,7 +593,6 @@ PyObject *CarState::Pickle (CarState *self_) noexcept !DictSetValue (dict.borrow (), "demo_respawn_timer", PyFloat_FromDouble (state.demoRespawnTimer))) return nullptr; - ::BallHitInfo const ballHitInfo{}; if ((state.ballHitInfo.relativePosOnBall != model.ballHitInfo.relativePosOnBall || state.ballHitInfo.ballPos != model.ballHitInfo.ballPos || state.ballHitInfo.extraHitVel != model.ballHitInfo.extraHitVel || @@ -602,11 +601,13 @@ PyObject *CarState::Pickle (CarState *self_) noexcept !DictSetValue (dict.borrow (), "ball_hit_info", PyNewRef (self_->ballHitInfo))) return nullptr; - ::CarControls const controls{}; - if ((state.lastControls.throttle != controls.throttle || state.lastControls.steer != controls.steer || - state.lastControls.pitch != controls.pitch || state.lastControls.yaw != controls.yaw || - state.lastControls.roll != controls.roll || state.lastControls.boost != controls.boost || - state.lastControls.jump != controls.jump || state.lastControls.handbrake != controls.handbrake) && + if ((state.lastControls.throttle != model.lastControls.throttle || + state.lastControls.steer != model.lastControls.steer || + state.lastControls.pitch != model.lastControls.pitch || state.lastControls.yaw != model.lastControls.yaw || + state.lastControls.roll != model.lastControls.roll || + state.lastControls.boost != model.lastControls.boost || + state.lastControls.jump != model.lastControls.jump || + state.lastControls.handbrake != model.lastControls.handbrake) && !DictSetValue (dict.borrow (), "last_controls", PyNewRef (self_->lastControls))) return nullptr; diff --git a/python-mtheall/Module.h b/python-mtheall/Module.h index ed06434a..9c7cb018 100644 --- a/python-mtheall/Module.h +++ b/python-mtheall/Module.h @@ -9,6 +9,7 @@ #include "Sim/Arena/Arena.h" #include "Sim/BallPredTracker/BallPredTracker.h" #include "Sim/Car/Car.h" +#include "Sim/GameEventTracker/GameEventTracker.h" #include #include @@ -77,8 +78,7 @@ class GIL PyGILState_Release (m_state); } - GIL () noexcept - : m_state (PyGILState_Ensure ()) + GIL () noexcept : m_state (PyGILState_Ensure ()) { } @@ -88,7 +88,7 @@ class GIL struct GameMode { - PyObject_HEAD + PyObject_HEAD; static PyTypeObject *Type; static PyType_Slot Slots[]; @@ -97,7 +97,7 @@ struct GameMode struct Team { - PyObject_HEAD + PyObject_HEAD; static PyTypeObject *Type; static PyType_Slot Slots[]; @@ -106,7 +106,7 @@ struct Team struct DemoMode { - PyObject_HEAD + PyObject_HEAD; static PyTypeObject *Type; static PyType_Slot Slots[]; @@ -115,7 +115,7 @@ struct DemoMode struct MemoryWeightMode { - PyObject_HEAD + PyObject_HEAD; static PyTypeObject *Type; static PyType_Slot Slots[]; @@ -124,7 +124,7 @@ struct MemoryWeightMode struct Vec { - PyObject_HEAD + PyObject_HEAD; ::Vec vec; @@ -156,7 +156,7 @@ struct Vec struct RotMat { - PyObject_HEAD + PyObject_HEAD; Vec *forward; Vec *right; @@ -193,7 +193,7 @@ struct RotMat struct Angle { - PyObject_HEAD + PyObject_HEAD; ::Angle angle; @@ -224,7 +224,7 @@ struct Angle struct BallHitInfo { - PyObject_HEAD + PyObject_HEAD; ::BallHitInfo info; @@ -258,7 +258,7 @@ struct BallHitInfo struct BallState { - PyObject_HEAD + PyObject_HEAD; ::BallState state; @@ -294,7 +294,7 @@ struct BallState struct Ball { - PyObject_HEAD + PyObject_HEAD; std::shared_ptr<::Arena> arena; ::Ball *ball; @@ -316,7 +316,7 @@ struct Ball struct BoostPadState { - PyObject_HEAD + PyObject_HEAD; ::BoostPadState state; @@ -341,7 +341,7 @@ struct BoostPadState struct BoostPad { - PyObject_HEAD + PyObject_HEAD; std::shared_ptr<::Arena> arena; ::BoostPad *pad; @@ -367,7 +367,7 @@ struct BoostPad struct WheelPairConfig { - PyObject_HEAD + PyObject_HEAD; ::WheelPairConfig config; Vec *connectionPointOffset; @@ -406,7 +406,7 @@ struct CarConfig MERC, }; - PyObject_HEAD + PyObject_HEAD; ::CarConfig config; @@ -443,7 +443,7 @@ struct CarConfig struct CarControls { - PyObject_HEAD + PyObject_HEAD; ::CarControls controls; @@ -470,7 +470,7 @@ struct CarControls struct CarState { - PyObject_HEAD + PyObject_HEAD; ::CarState state; @@ -514,7 +514,7 @@ struct CarState struct Car { - PyObject_HEAD + PyObject_HEAD; ::CarState demoState; @@ -523,6 +523,9 @@ struct Car unsigned goals; unsigned demos; unsigned boostPickups; + unsigned shots; + unsigned saves; + unsigned assists; static PyTypeObject *Type; static PyMemberDef Members[]; @@ -554,7 +557,7 @@ struct Car struct MutatorConfig { - PyObject_HEAD + PyObject_HEAD; ::MutatorConfig config; Vec *gravity; @@ -567,7 +570,8 @@ struct MutatorConfig static PyType_Spec Spec; static PyRef NewFromMutatorConfig (::MutatorConfig const &config_ = {::GameMode::SOCCAR}) noexcept; - static bool InitFromMutatorConfig (MutatorConfig *self_, ::MutatorConfig const &config_ = {::GameMode::SOCCAR}) noexcept; + static bool InitFromMutatorConfig (MutatorConfig *self_, + ::MutatorConfig const &config_ = {::GameMode::SOCCAR}) noexcept; static ::MutatorConfig ToMutatorConfig (MutatorConfig *self_) noexcept; static PyObject *New (PyTypeObject *subtype_, PyObject *args_, PyObject *kwds_) noexcept; @@ -585,7 +589,7 @@ struct Arena { class ThreadPool; - PyObject_HEAD + PyObject_HEAD; std::shared_ptr<::Arena> arena; std::shared_ptr threadPool; @@ -593,6 +597,7 @@ struct Arena std::unordered_map<::BoostPad *, PyRef> *boostPads; std::vector> *boostPadsByIndex; ::BallPredTracker *ballPrediction; + ::GameEventTracker *gameEvent; Ball *ball; PyObject *ballTouchCallback; @@ -605,6 +610,12 @@ struct Arena PyObject *carDemoCallbackUserData; PyObject *goalScoreCallback; PyObject *goalScoreCallbackUserData; + PyObject *shotEventCallback; + PyObject *shotEventCallbackUserData; + PyObject *goalEventCallback; + PyObject *goalEventCallbackUserData; + PyObject *saveEventCallback; + PyObject *saveEventCallbackUserData; unsigned blueScore; unsigned orangeScore; @@ -645,11 +656,14 @@ struct Arena static PyObject *ResetKickoff (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetBallTouchCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetBoostPickupCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; - static PyObject *SetCarBallCollision(Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; + static PyObject *SetCarBallCollision (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetCarBumpCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; - static PyObject *SetCarCarCollision(Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; + static PyObject *SetCarCarCollision (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetCarDemoCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetGoalScoreCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; + static PyObject *SetShotEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; + static PyObject *SetGoalEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; + static PyObject *SetSaveEventCallback (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *SetMutatorConfig (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *Step (Arena *self_, PyObject *args_, PyObject *kwds_) noexcept; static PyObject *Stop (Arena *self_) noexcept; @@ -663,6 +677,10 @@ struct Arena HandleCarBumpCallback (::Arena *arena_, ::Car *bumper_, ::Car *victim_, bool isDemo_, void *userData_) noexcept; static void HandleGoalScoreCallback (::Arena *arena_, ::Team scoringTeam_, void *userData_) noexcept; + static void HandleShotEventCallback (::Arena *arena_, ::Car *shooter_, ::Car *passer_, void *userData_) noexcept; + static void HandleGoalEventCallback (::Arena *arena_, ::Car *shooter_, ::Car *passer_, void *userData_) noexcept; + static void HandleSaveEventCallback (::Arena *arena_, ::Car *saver_, void *userData_) noexcept; + GETONLY_DECLARE (Arena, game_mode); GETONLY_DECLARE (Arena, tick_count); GETONLY_DECLARE (Arena, tick_rate); diff --git a/python-mtheall/unit_test.py b/python-mtheall/unit_test.py index f835456d..63a93bcf 100755 --- a/python-mtheall/unit_test.py +++ b/python-mtheall/unit_test.py @@ -937,6 +937,9 @@ def compare(self, car_a, car_b): self.assertEqual(car_a.goals, car_b.goals) self.assertEqual(car_a.demos, car_b.demos) self.assertEqual(car_a.boost_pickups, car_b.boost_pickups) + self.assertEqual(car_a.shots, car_b.shots) + self.assertEqual(car_a.saves, car_b.saves) + self.assertEqual(car_a.assists, car_b.assists) TestCarState.compare(self, car_a.get_state(), car_b.get_state()) TestCarConfig.compare(self, car_a.get_config(), car_b.get_config()) @@ -1596,6 +1599,154 @@ def test_get_ball_rot(self): ball_rot = arena.ball.get_rot() self.assertNotEqual(sum(ball_rot), 0) + def test_shot_event(self): + arena = rs.Arena(rs.GameMode.SOCCAR) + + car = arena.add_car(rs.Team.BLUE) + + ball_state = arena.ball.get_state() + ball_state.pos = rs.Vec(0.0, 4000.0, ball_state.pos.z) + arena.ball.set_state(ball_state) + + car_state = car.get_state() + car_state.pos = rs.Vec(0.0, 3000.0, car_state.pos.z) + car_state.vel = rs.Vec(0.0, 1000.0, 0.0) + car_state.ang_vel = rs.Vec(0.0, 0.0, 0.0) + + car_state.rot_mat.forward = rs.Vec( 0.0, 1.0, 0.0) + car_state.rot_mat.right = rs.Vec(-1.0, 0.0, 0.0) + car_state.rot_mat.up = rs.Vec( 0.0, 0.0, 1.0) + + car.set_state(car_state) + + shot = [False] + def handle_shot_event(arena: rs.Arena, shooter: rs.Car, passer: rs.Car, data): + data.assertIs(shooter, car) + data.assertIsNone(passer) + shot[0] = True + arena.stop() + + arena.set_shot_event_callback(handle_shot_event, self) + + for i in range(1000): + target_chase(arena.ball.get_state().pos, car) + arena.step(1) + + if shot[0]: + break + + self.assertTrue(shot[0]) + self.assertEqual(car.shots, 1) + + def test_save_event(self): + arena = rs.Arena(rs.GameMode.SOCCAR) + + car1 = arena.add_car(rs.Team.BLUE) + car2 = arena.add_car(rs.Team.ORANGE) + + ball_state = arena.ball.get_state() + ball_state.pos = rs.Vec(0.0, 4000.0, ball_state.pos.z) + arena.ball.set_state(ball_state) + + car_state = car1.get_state() + car_state.pos = rs.Vec(0.0, 3000.0, car_state.pos.z) + car_state.vel = rs.Vec(0.0, 1000.0, 0.0) + car_state.ang_vel = rs.Vec(0.0, 0.0, 0.0) + + car_state.rot_mat.forward = rs.Vec( 0.0, 1.0, 0.0) + car_state.rot_mat.right = rs.Vec(-1.0, 0.0, 0.0) + car_state.rot_mat.up = rs.Vec( 0.0, 0.0, 1.0) + + car1.set_state(car_state) + + car_state = car2.get_state() + car_state.pos = rs.Vec(0.0, 5000.0, car_state.pos.z) + car_state.vel = rs.Vec(0.0, 0.0, 0.0) + car_state.ang_vel = rs.Vec(0.0, 0.0, 0.0) + + car_state.rot_mat.forward = rs.Vec(0.0, -1.0, 0.0) + car_state.rot_mat.right = rs.Vec(1.0, 0.0, 0.0) + car_state.rot_mat.up = rs.Vec(0.0, 0.0, 1.0) + + car2.set_state(car_state) + car2.set_controls(rs.CarControls(jump=True)) + + saved = [False] + def handle_save_event(arena: rs.Arena, saver: rs.Car, data): + data.assertIs(saver, car2) + saved[0] = True + arena.stop() + + arena.set_save_event_callback(handle_save_event, self) + + for i in range(1000): + target_chase(arena.ball.get_state().pos, car1) + arena.step(1) + + if saved[0]: + break + + self.assertTrue(saved[0]) + self.assertEqual(car1.shots, 1) + self.assertEqual(car2.saves, 1) + + def test_goal_event(self): + arena = rs.Arena(rs.GameMode.SOCCAR) + + car1 = arena.add_car(rs.Team.BLUE) + car2 = arena.add_car(rs.Team.BLUE) + + ball_state = arena.ball.get_state() + ball_state.pos = rs.Vec(0.0, 4000.0, ball_state.pos.z) + arena.ball.set_state(ball_state) + + car_state = car1.get_state() + car_state.pos = rs.Vec(0.0, 3800.0, car_state.pos.z) + car_state.vel = rs.Vec(0.0, 0.0, 0.0) + car_state.ang_vel = rs.Vec(0.0, 0.0, 0.0) + + car_state.rot_mat.forward = rs.Vec( 0.0, 1.0, 0.0) + car_state.rot_mat.right = rs.Vec(-1.0, 0.0, 0.0) + car_state.rot_mat.up = rs.Vec( 0.0, 0.0, 1.0) + + car1.set_state(car_state) + + car_state = car2.get_state() + car_state.pos = rs.Vec(-100.0, 5120.0, car_state.pos.z) + car_state.vel = rs.Vec(0.0, 0.0, 0.0) + car_state.ang_vel = rs.Vec(0.0, 0.0, 0.0) + + car_state.rot_mat.forward = rs.Vec(1.0, 0.0, 0.0) + car_state.rot_mat.right = rs.Vec(0.0, 1.0, 0.0) + car_state.rot_mat.up = rs.Vec(0.0, 0.0, 1.0) + + car2.set_state(car_state) + + goal = [False] + def handle_goal_event(arena: rs.Arena, shooter: rs.Car, passer: rs.Car, data): + data.assertIs(shooter, car2) + data.assertIs(passer, car1) + goal[0] = True + arena.stop() + + arena.set_goal_event_callback(handle_goal_event, self) + + for i in range(1000): + target_chase(arena.ball.get_state().pos, car1) + + if i > 20: + car2.set_controls(rs.CarControls(jump=True)) + + arena.step(1) + + if goal[0]: + break + + self.assertTrue(goal[0]) + self.assertEqual(car1.assists, 1) + self.assertEqual(car2.goals, 1) + self.assertEqual(arena.blue_score, 1) + def test_get_gym_state(self): def load_mat3(mat: np.ndarray) -> glm.mat3: return glm.mat3(*mat) @@ -1756,8 +1907,8 @@ def check_pyr(pyr: np.ndarray) -> bool: self.assertEqual(car.id, gym_state[j][0]) self.assertEqual(car.team, gym_state[j][1]) self.assertEqual(car.goals, gym_state[j][2]) - #self.assertEqual(car.saves, gym_state[j][3]) - #self.assertEqual(car.shots, gym_state[j][4]) + self.assertEqual(car.saves, gym_state[j][3]) + self.assertEqual(car.shots, gym_state[j][4]) self.assertEqual(car.demos, gym_state[j][5]) self.assertEqual(car.boost_pickups, gym_state[j][6]) self.assertEqual(car_state.is_demoed, gym_state[j][7]) diff --git a/src/Sim/Arena/Arena.cpp b/src/Sim/Arena/Arena.cpp index 15923c6f..be659293 100644 --- a/src/Sim/Arena/Arena.cpp +++ b/src/Sim/Arena/Arena.cpp @@ -780,7 +780,7 @@ void Arena::Step(int ticksToSimulate) { if (_goalScoreCallback.func != NULL) { // Potentially fire goal score callback if (IsBallScored()) { - _goalScoreCallback.func(this, RS_TEAM_FROM_Y(ball->_rigidBody.m_worldTransform.m_origin.y()), _goalScoreCallback.userInfo); + _goalScoreCallback.func(this, RS_OPPOSITE_TEAM(RS_TEAM_FROM_Y(ball->_rigidBody.m_worldTransform.m_origin.y())), _goalScoreCallback.userInfo); } } diff --git a/src/Sim/GameMode.h b/src/Sim/GameMode.h index 91304e32..905d0fff 100644 --- a/src/Sim/GameMode.h +++ b/src/Sim/GameMode.h @@ -17,5 +17,6 @@ constexpr const char* GAMEMODE_STRS[] = { "soccar", "hoops", "heatseeker", - "snowday" -}; \ No newline at end of file + "snowday", + "the_void", +};