mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-25 00:13:27 +08:00
Merge pull request #658 from Dliwk/replay-rewind
Replay rewind/fast-forward
This commit is contained in:
commit
c0de5d079c
@ -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!)
|
||||
- Custom spaz "curse_time" values now work properly. (Thanks Temp!)
|
||||
- Implemented `efro.dataclassio.IOMultiType` which will make my life a lot
|
||||
easier.
|
||||
|
||||
@ -120,6 +120,7 @@ from _bascenev1 import (
|
||||
release_keyboard_input,
|
||||
reset_random_player_names,
|
||||
resume_replay,
|
||||
seek_replay,
|
||||
broadcastmessage,
|
||||
SessionData,
|
||||
SessionPlayer,
|
||||
@ -404,6 +405,7 @@ __all__ = [
|
||||
'release_keyboard_input',
|
||||
'reset_random_player_names',
|
||||
'resume_replay',
|
||||
'seek_replay',
|
||||
'safecolor',
|
||||
'screenmessage',
|
||||
'SceneV1AppMode',
|
||||
|
||||
@ -528,6 +528,62 @@ 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.WeakCall(self._rewind_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,
|
||||
)
|
||||
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.WeakCall(self._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 _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]]
|
||||
|
||||
@ -1568,6 +1568,39 @@ static PyMethodDef PyResumeReplayDef = {
|
||||
"Resumes replay.",
|
||||
};
|
||||
|
||||
// -------------------------- seek_replay --------------------------------------
|
||||
|
||||
static auto PySeekReplay(PyObject* self, PyObject* args) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
auto* appmode = SceneV1AppMode::GetActiveOrThrow();
|
||||
auto* session =
|
||||
dynamic_cast<ClientSessionReplay*>(appmode->GetForegroundSession());
|
||||
if (session == nullptr) {
|
||||
throw Exception(
|
||||
"Attempting to seek a replay not in replay session context.");
|
||||
}
|
||||
float delta;
|
||||
if (!PyArg_ParseTuple(args, "f", &delta)) {
|
||||
return nullptr;
|
||||
}
|
||||
session->SeekTo(session->base_time()
|
||||
+ static_cast<millisecs_t>(delta * 1'000));
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
static PyMethodDef PySeekReplayDef = {
|
||||
"seek_replay", // name
|
||||
PySeekReplay, // method
|
||||
METH_VARARGS, // flags
|
||||
|
||||
"seek_replay(delta: float) -> None\n"
|
||||
"\n"
|
||||
"(internal)\n"
|
||||
"\n"
|
||||
"Rewind or fast-forward replay.",
|
||||
};
|
||||
|
||||
// ----------------------- reset_random_player_names ---------------------------
|
||||
|
||||
static auto PyResetRandomPlayerNames(PyObject* self, PyObject* args,
|
||||
@ -1846,6 +1879,7 @@ auto PythonMethodsScene::GetMethods() -> std::vector<PyMethodDef> {
|
||||
PySetReplaySpeedExponentDef,
|
||||
PyGetReplaySpeedExponentDef,
|
||||
PyIsReplayPausedDef,
|
||||
PySeekReplayDef,
|
||||
PyPauseReplayDef,
|
||||
PyResumeReplayDef,
|
||||
PySetDebugSpeedExponentDef,
|
||||
|
||||
@ -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<uint8_t>& command);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include "ballistica/scene_v1/support/client_session_replay.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#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<double>(fast_forward_base_time_ - base_time()));
|
||||
}
|
||||
is_fast_forwarding_ = false;
|
||||
}
|
||||
auto* appmode = SceneV1AppMode::GetActiveOrFatal();
|
||||
if (appmode->is_replay_paused()) {
|
||||
return 0.0;
|
||||
// FIXME: seeking a replay results in black screen here
|
||||
return 0;
|
||||
}
|
||||
return base_advance_millisecs * pow(2.0f, appmode->replay_speed_exponent());
|
||||
}
|
||||
@ -134,6 +147,23 @@ 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
|
||||
// if we didn't that for too long.
|
||||
if (base_time() >= (states_.empty() ? 0 : states_.back().base_time_)
|
||||
+ kReplayStateDumpIntervalMillisecs) {
|
||||
SessionStream out(nullptr, false);
|
||||
DumpFullState(&out);
|
||||
|
||||
current_state_.base_time_ = base_time();
|
||||
current_state_.correction_messages_.clear();
|
||||
GetCorrectionMessages(false, ¤t_state_.correction_messages_);
|
||||
|
||||
fflush(file_);
|
||||
current_state_.file_position_ = ftell(file_);
|
||||
current_state_.message_ = out.GetOutMessage();
|
||||
states_.push_back(current_state_);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> buffer;
|
||||
uint8_t len8;
|
||||
uint32_t len32;
|
||||
@ -201,7 +231,6 @@ void ClientSessionReplay::FetchMessages() {
|
||||
for (auto&& i : connections_to_clients_) {
|
||||
i->SendReliableMessage(data_decompressed);
|
||||
}
|
||||
message_fetch_num_++;
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,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<uint8_t>(1, BA_MESSAGE_SESSION_RESET));
|
||||
@ -265,4 +298,52 @@ void ClientSessionReplay::OnReset(bool rewind) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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() {
|
||||
// FIXME: calling reset here causes background music to start over
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ballistica::scene_v1
|
||||
|
||||
@ -29,8 +29,29 @@ class ClientSessionReplay : public ClientSession,
|
||||
void Error(const std::string& description) override;
|
||||
void FetchMessages() override;
|
||||
|
||||
void SeekTo(millisecs_t to_base_time);
|
||||
|
||||
private:
|
||||
uint32_t message_fetch_num_{};
|
||||
struct IntermediateState {
|
||||
// Message containing full scene state at the moment.
|
||||
std::vector<uint8_t> message_;
|
||||
std::vector<std::vector<uint8_t>> correction_messages_;
|
||||
|
||||
// A position in replay file where we should continue from.
|
||||
int64_t file_position_;
|
||||
|
||||
millisecs_t base_time_;
|
||||
};
|
||||
|
||||
void RestoreFromCurrentState();
|
||||
|
||||
// List of passed states which we can rewind to.
|
||||
std::vector<IntermediateState> states_;
|
||||
IntermediateState current_state_;
|
||||
|
||||
bool is_fast_forwarding_{};
|
||||
millisecs_t fast_forward_base_time_{};
|
||||
|
||||
bool have_sent_client_message_{};
|
||||
std::vector<ConnectionToClient*> connections_to_clients_;
|
||||
std::vector<ConnectionToClient*> connections_to_clients_ignored_;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user