From 5bd7e057730bda4c1c2b8b29ac580ba90eee3ab2 Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Mon, 8 Jan 2024 14:38:04 +0300 Subject: [PATCH 1/7] work on replay rewind --- .../ba_data/python/bascenev1/__init__.py | 2 + .../python/methods/python_methods_scene.cc | 29 ++++++++++++++ .../scene_v1/support/client_session_replay.cc | 38 +++++++++++++++++++ .../scene_v1/support/client_session_replay.h | 16 ++++++++ 4 files changed, 85 insertions(+) diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py index 25c1f5ef..44d7276d 100644 --- a/src/assets/ba_data/python/bascenev1/__init__.py +++ b/src/assets/ba_data/python/bascenev1/__init__.py @@ -120,6 +120,7 @@ from _bascenev1 import ( release_keyboard_input, reset_random_player_names, resume_replay, + rewind_replay, broadcastmessage, SessionData, SessionPlayer, @@ -400,6 +401,7 @@ __all__ = [ 'release_keyboard_input', 'reset_random_player_names', 'resume_replay', + 'rewind_replay', 'safecolor', 'screenmessage', 'SceneV1AppMode', diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index 5e74a112..389bfc70 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -1568,6 +1568,34 @@ static PyMethodDef PyResumeReplayDef = { "Resumes replay.", }; +// ------------------------ rewind_replay -------------------------------------- + +static auto PyRewindReplay(PyObject* self, PyObject* args) -> PyObject* { + BA_PYTHON_TRY; + auto* appmode = SceneV1AppMode::GetActiveOrThrow(); + auto* session = + dynamic_cast(appmode->GetForegroundSession()); + if (session == nullptr) { + throw Exception( + "Attempting to rewind a replay not in replay session context."); + } + session->RestoreState(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyRewindReplayDef = { + "rewind_replay", // name + PyRewindReplay, // method + METH_VARARGS, // flags + + "rewind_replay() -> None\n" + "\n" + "(internal)\n" + "\n" + "Rewinds replay.", +}; + // ----------------------- reset_random_player_names --------------------------- static auto PyResetRandomPlayerNames(PyObject* self, PyObject* args, @@ -1846,6 +1874,7 @@ auto PythonMethodsScene::GetMethods() -> std::vector { PySetReplaySpeedExponentDef, PyGetReplaySpeedExponentDef, PyIsReplayPausedDef, + PyRewindReplayDef, PyPauseReplayDef, PyResumeReplayDef, PySetDebugSpeedExponentDef, diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index f5c63095..6fe35bd5 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -134,6 +134,17 @@ void ClientSessionReplay::FetchMessages() { // If we have no messages left, read from the file until we get some. while (commands().empty()) { + { + // Before we read next message, let's save our current state. + SessionStream out(nullptr, false); + DumpFullState(&out); + + fflush(file_); + current_state_.file_position_ = ftell(file_); + current_state_.message_ = out.GetOutMessage(); + SaveState(); + } + std::vector buffer; uint8_t len8; uint32_t len32; @@ -265,4 +276,31 @@ void ClientSessionReplay::OnReset(bool rewind) { } } +void ClientSessionReplay::SaveState() { states_.push_back(current_state_); } + +void ClientSessionReplay::RestoreState() { + ScreenMessage("restoring state " + std::to_string(states_.size())); + const int N = 500; + + if (states_.size() <= N) { + states_.clear(); + Reset(true); + return; + } + + for (int i = 0; i < N; ++i) { + states_.pop_back(); + } + + current_state_ = states_.back(); + RestoreFromCurrentState(); +} + +void ClientSessionReplay::RestoreFromCurrentState() { + // what to do with messages_fetch_num_? is it used somewhere at all? + Reset(true); + fseek(file_, current_state_.file_position_, SEEK_SET); + HandleSessionMessage(current_state_.message_); +} + } // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h index 4bd82e82..446b70f3 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.h +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -28,8 +28,24 @@ class ClientSessionReplay : public ClientSession, void Error(const std::string& description) override; void FetchMessages() override; + void SaveState(); + void RestoreState(); private: + struct IntermediateState { + // Message containing full scene state at the moment. + std::vector message_; + + // A position in replay file where we should continue from. + long file_position_; + }; + + void RestoreFromCurrentState(); + + // List of passed states which we can rewind to. + std::vector states_; + IntermediateState current_state_; + uint32_t message_fetch_num_{}; bool have_sent_client_message_{}; std::vector connections_to_clients_; From 13ad00029c835ffa45679388e10e440850b3bd8d Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Mon, 8 Jan 2024 17:29:43 +0300 Subject: [PATCH 2/7] add dynamics correction messages --- src/ballistica/scene_v1/support/client_session_replay.cc | 6 ++++++ src/ballistica/scene_v1/support/client_session_replay.h | 1 + 2 files changed, 7 insertions(+) diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index 6fe35bd5..528054d3 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -139,6 +139,9 @@ void ClientSessionReplay::FetchMessages() { SessionStream out(nullptr, false); DumpFullState(&out); + current_state_.correction_messages_.clear(); + GetCorrectionMessages(false, ¤t_state_.correction_messages_); + fflush(file_); current_state_.file_position_ = ftell(file_); current_state_.message_ = out.GetOutMessage(); @@ -301,6 +304,9 @@ void ClientSessionReplay::RestoreFromCurrentState() { Reset(true); fseek(file_, current_state_.file_position_, SEEK_SET); HandleSessionMessage(current_state_.message_); + for (const auto& msg : current_state_.correction_messages_) { + HandleSessionMessage(msg); + } } } // namespace ballistica::scene_v1 diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h index 446b70f3..35db9483 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.h +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -35,6 +35,7 @@ class ClientSessionReplay : public ClientSession, struct IntermediateState { // Message containing full scene state at the moment. std::vector message_; + std::vector> correction_messages_; // A position in replay file where we should continue from. long file_position_; From e3ef47dfc0733d6183af319ab6d82509799d5c79 Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Mon, 8 Jan 2024 17:51:52 +0300 Subject: [PATCH 3/7] work on paused replays --- .../python/methods/python_methods_scene.cc | 2 +- .../scene_v1/support/client_session.h | 6 +++++ .../scene_v1/support/client_session_replay.cc | 27 ++++++++++--------- .../scene_v1/support/client_session_replay.h | 4 ++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index 389bfc70..b31f87be 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -1579,7 +1579,7 @@ static auto PyRewindReplay(PyObject* self, PyObject* args) -> PyObject* { throw Exception( "Attempting to rewind a replay not in replay session context."); } - session->RestoreState(); + session->RestoreState(session->base_time() - 2'000); Py_RETURN_NONE; BA_PYTHON_CATCH; } diff --git a/src/ballistica/scene_v1/support/client_session.h b/src/ballistica/scene_v1/support/client_session.h index f8e2acfb..e9c43d7b 100644 --- a/src/ballistica/scene_v1/support/client_session.h +++ b/src/ballistica/scene_v1/support/client_session.h @@ -96,6 +96,12 @@ class ClientSession : public Session { target_base_time_millisecs_ = base_time_millisecs_; } + protected: + void SetBaseTime(millisecs_t time) { + base_time_millisecs_ = time; + ResetTargetBaseTime(); + } + private: void ClearSessionObjs(); void AddCommand(const std::vector& command); diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index 528054d3..b6b2cce6 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -18,7 +18,7 @@ auto ClientSessionReplay::GetActualTimeAdvanceMillisecs( double base_advance_millisecs) -> double { auto* appmode = SceneV1AppMode::GetActiveOrFatal(); if (appmode->is_replay_paused()) { - return 0.0; + return 0.001; } return base_advance_millisecs * pow(2.0f, appmode->replay_speed_exponent()); } @@ -139,6 +139,7 @@ void ClientSessionReplay::FetchMessages() { SessionStream out(nullptr, false); DumpFullState(&out); + current_state_.base_time_ = base_time(); current_state_.correction_messages_.clear(); GetCorrectionMessages(false, ¤t_state_.correction_messages_); @@ -281,28 +282,28 @@ void ClientSessionReplay::OnReset(bool rewind) { void ClientSessionReplay::SaveState() { states_.push_back(current_state_); } -void ClientSessionReplay::RestoreState() { - ScreenMessage("restoring state " + std::to_string(states_.size())); - const int N = 500; +void ClientSessionReplay::RestoreState(millisecs_t to_base_time) { + ScreenMessage("was: " + std::to_string(base_time()) + "ms"); + ScreenMessage("want: " + std::to_string(to_base_time) + "ms"); - if (states_.size() <= N) { - states_.clear(); - Reset(true); - return; - } - - for (int i = 0; i < N; ++i) { + while (!states_.empty() && states_.back().base_time_ > to_base_time) { states_.pop_back(); } - current_state_ = states_.back(); - RestoreFromCurrentState(); + if (states_.empty()) { + Reset(true); + } else { + current_state_ = states_.back(); + RestoreFromCurrentState(); + } } void ClientSessionReplay::RestoreFromCurrentState() { // what to do with messages_fetch_num_? is it used somewhere at all? Reset(true); fseek(file_, current_state_.file_position_, SEEK_SET); + + SetBaseTime(current_state_.base_time_); HandleSessionMessage(current_state_.message_); for (const auto& msg : current_state_.correction_messages_) { HandleSessionMessage(msg); diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h index 35db9483..32adbeeb 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.h +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -29,7 +29,7 @@ class ClientSessionReplay : public ClientSession, void Error(const std::string& description) override; void FetchMessages() override; void SaveState(); - void RestoreState(); + void RestoreState(millisecs_t to_base_time); private: struct IntermediateState { @@ -39,6 +39,8 @@ class ClientSessionReplay : public ClientSession, // A position in replay file where we should continue from. long file_position_; + + millisecs_t base_time_; }; void RestoreFromCurrentState(); From 069cd054a050eead03d81fdf057fc0eb3e038315 Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Mon, 8 Jan 2024 18:08:07 +0300 Subject: [PATCH 4/7] decrease mem usage (though still linear to replay size) --- .../scene_v1/support/client_session_replay.cc | 11 ++++++++--- .../scene_v1/support/client_session_replay.h | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index b6b2cce6..0c70280e 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -134,8 +134,10 @@ void ClientSessionReplay::FetchMessages() { // If we have no messages left, read from the file until we get some. while (commands().empty()) { - { - // Before we read next message, let's save our current state. + // Before we read next message, let's save our current state + // if we didn't that for too long. + unsaved_messages_count_ += 1; + if (unsaved_messages_count_ > 50) { SessionStream out(nullptr, false); DumpFullState(&out); @@ -280,7 +282,10 @@ void ClientSessionReplay::OnReset(bool rewind) { } } -void ClientSessionReplay::SaveState() { states_.push_back(current_state_); } +void ClientSessionReplay::SaveState() { + unsaved_messages_count_ = 0; + states_.push_back(current_state_); +} void ClientSessionReplay::RestoreState(millisecs_t to_base_time) { ScreenMessage("was: " + std::to_string(base_time()) + "ms"); diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h index 32adbeeb..5a7f07ae 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.h +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -49,6 +49,8 @@ class ClientSessionReplay : public ClientSession, std::vector states_; IntermediateState current_state_; + int unsaved_messages_count_{}; + uint32_t message_fetch_num_{}; bool have_sent_client_message_{}; std::vector connections_to_clients_; From d45caaea92e98fb26a49fe156a01922bcb0828e6 Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Thu, 25 Jan 2024 00:03:44 +0300 Subject: [PATCH 5/7] fast forwarding replays --- .../ba_data/python/bascenev1/__init__.py | 4 +- .../python/methods/python_methods_scene.cc | 27 ++++--- .../scene_v1/support/client_session_replay.cc | 77 +++++++++++++------ .../scene_v1/support/client_session_replay.h | 10 +-- 4 files changed, 77 insertions(+), 41 deletions(-) diff --git a/src/assets/ba_data/python/bascenev1/__init__.py b/src/assets/ba_data/python/bascenev1/__init__.py index 44d7276d..d0e1850e 100644 --- a/src/assets/ba_data/python/bascenev1/__init__.py +++ b/src/assets/ba_data/python/bascenev1/__init__.py @@ -120,7 +120,7 @@ from _bascenev1 import ( release_keyboard_input, reset_random_player_names, resume_replay, - rewind_replay, + seek_replay, broadcastmessage, SessionData, SessionPlayer, @@ -401,7 +401,7 @@ __all__ = [ 'release_keyboard_input', 'reset_random_player_names', 'resume_replay', - 'rewind_replay', + 'seek_replay', 'safecolor', 'screenmessage', 'SceneV1AppMode', diff --git a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc index b31f87be..8b94b11e 100644 --- a/src/ballistica/scene_v1/python/methods/python_methods_scene.cc +++ b/src/ballistica/scene_v1/python/methods/python_methods_scene.cc @@ -1568,32 +1568,37 @@ static PyMethodDef PyResumeReplayDef = { "Resumes replay.", }; -// ------------------------ rewind_replay -------------------------------------- +// -------------------------- seek_replay -------------------------------------- -static auto PyRewindReplay(PyObject* self, PyObject* args) -> PyObject* { +static auto PySeekReplay(PyObject* self, PyObject* args) -> PyObject* { BA_PYTHON_TRY; auto* appmode = SceneV1AppMode::GetActiveOrThrow(); auto* session = dynamic_cast(appmode->GetForegroundSession()); if (session == nullptr) { throw Exception( - "Attempting to rewind a replay not in replay session context."); + "Attempting to seek a replay not in replay session context."); } - session->RestoreState(session->base_time() - 2'000); + float delta; + if (!PyArg_ParseTuple(args, "f", &delta)) { + return nullptr; + } + session->SeekTo(session->base_time() + + static_cast(delta * 1'000)); Py_RETURN_NONE; BA_PYTHON_CATCH; } -static PyMethodDef PyRewindReplayDef = { - "rewind_replay", // name - PyRewindReplay, // method - METH_VARARGS, // flags +static PyMethodDef PySeekReplayDef = { + "seek_replay", // name + PySeekReplay, // method + METH_VARARGS, // flags - "rewind_replay() -> None\n" + "seek_replay(delta: float) -> None\n" "\n" "(internal)\n" "\n" - "Rewinds replay.", + "Rewind or fast-forward replay.", }; // ----------------------- reset_random_player_names --------------------------- @@ -1874,7 +1879,7 @@ auto PythonMethodsScene::GetMethods() -> std::vector { PySetReplaySpeedExponentDef, PyGetReplaySpeedExponentDef, PyIsReplayPausedDef, - PyRewindReplayDef, + PySeekReplayDef, PyPauseReplayDef, PyResumeReplayDef, PySetDebugSpeedExponentDef, diff --git a/src/ballistica/scene_v1/support/client_session_replay.cc b/src/ballistica/scene_v1/support/client_session_replay.cc index 0c70280e..cd60b10b 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.cc +++ b/src/ballistica/scene_v1/support/client_session_replay.cc @@ -2,6 +2,8 @@ #include "ballistica/scene_v1/support/client_session_replay.h" +#include + #include "ballistica/base/assets/assets.h" #include "ballistica/base/networking/networking.h" #include "ballistica/base/support/huffman.h" @@ -14,11 +16,22 @@ namespace ballistica::scene_v1 { +static const millisecs_t kReplayStateDumpIntervalMillisecs = 500; + auto ClientSessionReplay::GetActualTimeAdvanceMillisecs( double base_advance_millisecs) -> double { + if (is_fast_forwarding_) { + if (base_time() < fast_forward_base_time_) { + return std::min( + base_advance_millisecs * 8, + static_cast(fast_forward_base_time_ - base_time())); + } + is_fast_forwarding_ = false; + } auto* appmode = SceneV1AppMode::GetActiveOrFatal(); if (appmode->is_replay_paused()) { - return 0.001; + // FIXME: seeking a replay results in black screen here + return 0; } return base_advance_millisecs * pow(2.0f, appmode->replay_speed_exponent()); } @@ -136,8 +149,8 @@ void ClientSessionReplay::FetchMessages() { while (commands().empty()) { // Before we read next message, let's save our current state // if we didn't that for too long. - unsaved_messages_count_ += 1; - if (unsaved_messages_count_ > 50) { + if (base_time() >= (states_.empty() ? 0 : states_.back().base_time_) + + kReplayStateDumpIntervalMillisecs) { SessionStream out(nullptr, false); DumpFullState(&out); @@ -148,7 +161,7 @@ void ClientSessionReplay::FetchMessages() { fflush(file_); current_state_.file_position_ = ftell(file_); current_state_.message_ = out.GetOutMessage(); - SaveState(); + states_.push_back(current_state_); } std::vector buffer; @@ -218,7 +231,6 @@ void ClientSessionReplay::FetchMessages() { for (auto&& i : connections_to_clients_) { i->SendReliableMessage(data_decompressed); } - message_fetch_num_++; } } @@ -238,6 +250,10 @@ void ClientSessionReplay::OnReset(bool rewind) { // Handles base resetting. ClientSession::OnReset(rewind); + // Hack or not, but let's reset our fast-forward flag here, in case we were + // asked to seek replay further than it's length. + is_fast_forwarding_ = false; + // If we've got any clients attached to us, tell them to reset as well. for (auto&& i : connections_to_clients_) { i->SendReliableMessage(std::vector(1, BA_MESSAGE_SESSION_RESET)); @@ -282,29 +298,44 @@ void ClientSessionReplay::OnReset(bool rewind) { } } -void ClientSessionReplay::SaveState() { - unsaved_messages_count_ = 0; - states_.push_back(current_state_); -} - -void ClientSessionReplay::RestoreState(millisecs_t to_base_time) { - ScreenMessage("was: " + std::to_string(base_time()) + "ms"); - ScreenMessage("want: " + std::to_string(to_base_time) + "ms"); - - while (!states_.empty() && states_.back().base_time_ > to_base_time) { - states_.pop_back(); - } - - if (states_.empty()) { - Reset(true); +void ClientSessionReplay::SeekTo(millisecs_t to_base_time) { + is_fast_forwarding_ = false; + if (to_base_time < base_time()) { + auto it = std::lower_bound( + states_.rbegin(), states_.rend(), to_base_time, + [&](const IntermediateState& state, millisecs_t time) -> bool { + return state.base_time_ > time; + }); + if (it == states_.rend()) { + Reset(true); + } else { + current_state_ = *it; + RestoreFromCurrentState(); + } } else { - current_state_ = states_.back(); - RestoreFromCurrentState(); + auto it = std::lower_bound( + states_.begin(), states_.end(), to_base_time, + [&](const IntermediateState& state, millisecs_t time) -> bool { + return state.base_time_ < time; + }); + if (it == states_.end()) { + if (!states_.empty()) { + current_state_ = states_.back(); + RestoreFromCurrentState(); + } + // Let's speed up replay a bit + // (and we'll collect needed states along). + is_fast_forwarding_ = true; + fast_forward_base_time_ = to_base_time; + } else { + current_state_ = *it; + RestoreFromCurrentState(); + } } } void ClientSessionReplay::RestoreFromCurrentState() { - // what to do with messages_fetch_num_? is it used somewhere at all? + // FIXME: calling reset here causes background music to start over Reset(true); fseek(file_, current_state_.file_position_, SEEK_SET); diff --git a/src/ballistica/scene_v1/support/client_session_replay.h b/src/ballistica/scene_v1/support/client_session_replay.h index 5a7f07ae..ae5b8f72 100644 --- a/src/ballistica/scene_v1/support/client_session_replay.h +++ b/src/ballistica/scene_v1/support/client_session_replay.h @@ -28,8 +28,8 @@ class ClientSessionReplay : public ClientSession, void Error(const std::string& description) override; void FetchMessages() override; - void SaveState(); - void RestoreState(millisecs_t to_base_time); + + void SeekTo(millisecs_t to_base_time); private: struct IntermediateState { @@ -38,7 +38,7 @@ class ClientSessionReplay : public ClientSession, std::vector> correction_messages_; // A position in replay file where we should continue from. - long file_position_; + int64_t file_position_; millisecs_t base_time_; }; @@ -49,9 +49,9 @@ class ClientSessionReplay : public ClientSession, std::vector states_; IntermediateState current_state_; - int unsaved_messages_count_{}; + bool is_fast_forwarding_{}; + millisecs_t fast_forward_base_time_{}; - uint32_t message_fetch_num_{}; bool have_sent_client_message_{}; std::vector connections_to_clients_; std::vector connections_to_clients_ignored_; From e0a44cdc2f61e931f4da751a450079a9f4980010 Mon Sep 17 00:00:00 2001 From: Vishal Date: Sat, 13 Jan 2024 16:13:45 +0300 Subject: [PATCH 6/7] Update mainmenu.py --- .../ba_data/python/bauiv1lib/mainmenu.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index bec93e9c..07eee0f1 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -526,6 +526,56 @@ class MainMenuWindow(bui.Window): autoselect=True, on_activate_call=bui.Call(self._pause_or_resume_replay), ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + h - b_size * 1.5 - b_buffer_1 * 2, + v - b_size - b_buffer_2 + v_offs, + ), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.Call(self._change_replay_speed, -1), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='<<', + position=( + h - b_size - b_buffer_1 * 2, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=2.0 * t_scale, + ) + btn = bui.buttonwidget( + parent=self._root_widget, + position=( + h + b_size * 0.5 + b_buffer_1 * 2, + v - b_size - b_buffer_2 + v_offs, + ), + button_type='square', + size=(b_size, b_size), + label='', + autoselect=True, + on_activate_call=bui.Call(bs.forward_replay), + ) + bui.textwidget( + parent=self._root_widget, + draw_controller=btn, + text='>>', + position=( + h + b_size + b_buffer_1 * 2, + v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs, + ), + h_align='center', + v_align='center', + size=(0, 0), + scale=2.0 * t_scale, + ) def _refresh_not_in_game( self, positions: list[tuple[float, float, float]] From dad50e2141e1962294cd508dfc6464df32e774af Mon Sep 17 00:00:00 2001 From: Roman Trapeznikov Date: Thu, 25 Jan 2024 23:24:37 +0300 Subject: [PATCH 7/7] wired up ui --- CHANGELOG.md | 1 + src/assets/ba_data/python/bauiv1lib/mainmenu.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f77a1e..070f0e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ languages; I feel it helps keep logic more understandable and should help us catch problems where a base class changes or removes a method and child classes forget to adapt to the change. +- Replays now have rewind/fast-forward buttons!! (Thanks Dliwk, vishal332008!) ### 1.7.32 (build 21741, api 8, 2023-12-20) - Fixed a screen message that no one will ever see (Thanks vishal332008?...) diff --git a/src/assets/ba_data/python/bauiv1lib/mainmenu.py b/src/assets/ba_data/python/bauiv1lib/mainmenu.py index 07eee0f1..65457bdf 100644 --- a/src/assets/ba_data/python/bauiv1lib/mainmenu.py +++ b/src/assets/ba_data/python/bauiv1lib/mainmenu.py @@ -536,7 +536,7 @@ class MainMenuWindow(bui.Window): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.Call(self._change_replay_speed, -1), + on_activate_call=bui.WeakCall(self._rewind_replay), ) bui.textwidget( parent=self._root_widget, @@ -561,7 +561,7 @@ class MainMenuWindow(bui.Window): size=(b_size, b_size), label='', autoselect=True, - on_activate_call=bui.Call(bs.forward_replay), + on_activate_call=bui.WeakCall(self._forward_replay), ) bui.textwidget( parent=self._root_widget, @@ -577,6 +577,12 @@ class MainMenuWindow(bui.Window): scale=2.0 * t_scale, ) + def _rewind_replay(self) -> None: + bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent())) + + def _forward_replay(self) -> None: + bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent())) + def _refresh_not_in_game( self, positions: list[tuple[float, float, float]] ) -> tuple[float, float, float]: