cakefoot/src/Replay.cpp
Cocktail Frank 927de132be Remove calls to JSON constructor which use initializer list
There is a bug in the JSON library which causes the initializer list
constructor to inconsistently create either an array or a scalar value.

This bug causes inconsistent behavoir between the PC and WASM builds.

See https://json.nlohmann.me/home/faq/#known-bugs
2025-09-17 19:54:28 -04:00

247 lines
6.7 KiB
C++

#include "Replay.hpp"
cakefoot::Replay::KeyFrame cakefoot::Replay::latest_key_frame() const
{
return progress.read(key_frames_key).back().get<KeyFrame>();
}
void cakefoot::Replay::load(const fs::path& path)
{
progress.load(path);
most_recent_path = path;
}
void cakefoot::Replay::record(KeyFrame frame)
{
/* The key frames array will automatically be created by nlohmann::json if it doesn't exist */
key_frames().push_back(frame);
}
void cakefoot::Replay::save(
int level,
const nlohmann::json& metadata,
const fs::path& directory,
const std::string save_data_id)
{
std::ostringstream file_name;
/* The file name is the level index followed by a time stamp followed by the file ID. */
file_name << std::setfill('0') << std::setw(2) << level << "_" <<
sb::time::minute_stamp(std::chrono::system_clock::now()) << "_" << progress.id() << ".json";
fs::path path { directory / file_name.str() };
sb::Log::Multi() << "Writing replay to " << path << sb::Log::end;
/* Add metadata from user if requested. Add level index and length to the metadata. */
progress.set(metadata, metadata_key);
progress.set(level, metadata_key, level_metadata_key);
std::ostringstream length_formatted;
length_formatted << std::setprecision(2) << length();
progress.set(length_formatted.str(), metadata_key, length_metadata_key);
/* Associate with a save data file if ID is given */
if (!save_data_id.empty())
{
progress.set(save_data_id, save_data_id_key);
}
/* Add the start time to the file */
std::chrono::time_point<std::chrono::system_clock> start_time {
std::chrono::system_clock::now() - std::chrono::seconds(static_cast<int>(length()))
};
progress.set(sb::time::minute_stamp(start_time), start_time_key);
/* Progress::save takes care of creating the directory if necessary */
progress.save(path, true, false);
/* Save the path, so data can be removed if necessary */
most_recent_path = path;
}
void cakefoot::Replay::remove()
{
if (fs::exists(most_recent_path))
{
try
{
fs::remove(most_recent_path);
sb::Log::Multi() << "Removed replay at " << most_recent_path << sb::Log::end;
}
catch (fs::filesystem_error error)
{
sb::Log::Multi() << "Error removing replay at " << most_recent_path << ": " << error.what() <<
sb::Log::end;
}
}
most_recent_path.clear();
}
std::vector<cakefoot::Replay::KeyFrame> cakefoot::Replay::unread(float time)
{
std::vector<KeyFrame> frames;
/* Get a list of all unread frames until the given time stamp */
while (playback_index < key_frames().size() && elapsed(playback_index) <= time)
{
const KeyFrame& frame { key_frames().at(playback_index++).get<KeyFrame>() };
frames.push_back(frame);
/* Mark event progress */
if (frame.event == coin)
{
coin_taken = true;
}
else if (frame.event == collect)
{
coin_collected = true;
}
else if (frame.event == collision && !coin_collected)
{
coin_taken = false;
}
else if (frame.event == checkpoint)
{
checkpoints++;
}
else if (frame.event == end)
{
end_event_encountered = true;
}
}
if (!frames.empty())
{
/* Calculate the speed based on movement between frames */
if (playback_index > 1 && frames.back().event != end)
{
KeyFrame previous { key_frames().at(playback_index - 2).get<KeyFrame>() };
int mirror { frames.back().mirrored ? -1 : 1 };
float speed {
(mirror * (frames.back().translation.x - previous.translation.x) >= 0.0f ? 1 : -1)
* glm::distance(frames.back().translation, previous.translation)
* (1.0f / std::max(frames.back().time - previous.time, 0.0001f))
};
frames.back().speed = speed;
}
else
{
frames.back().speed = 0.0f;
}
/* Store the most recently read frame for fast access */
_last_read = frames.back();
}
return frames;
}
cakefoot::Replay::KeyFrame cakefoot::Replay::last_read() const
{
return _last_read;
}
void cakefoot::Replay::reset()
{
playback_index = 0;
coin_taken = false;
coin_collected = false;
checkpoints = 0;
end_event_encountered = false;
}
float cakefoot::Replay::length() const
{
if (!empty())
{
return key_frames().at(key_frames().size() - 1).get<KeyFrame>().time -
key_frames().front().get<KeyFrame>().time;
}
else
{
return 0.0f;
}
}
bool cakefoot::Replay::empty() const
{
return !progress.contains(key_frames_key) || key_frames().size() == 0;
}
float cakefoot::Replay::elapsed(int index) const
{
return length() > 0.0f ? elapsed(key_frames().at(index).get<KeyFrame>()) : 0.0f;
}
float cakefoot::Replay::elapsed(KeyFrame frame) const
{
return length() > 0.0f ? frame.time - key_frames().front().get<KeyFrame>().time : 0.0f;
}
std::string cakefoot::Replay::id() const
{
return progress.id();
}
bool cakefoot::Replay::ended() const
{
return end_event_encountered;
}
nlohmann::json cakefoot::Replay::metadata() const
{
if (progress.contains(metadata_key))
{
return progress.read(metadata_key);
}
else
{
return {};
}
}
const nlohmann::json& cakefoot::Replay::json() const
{
return progress.read();
}
void cakefoot::Replay::clear()
{
key_frames().clear();
}
void cakefoot::to_json(nlohmann::json& json, const Replay::KeyFrame& key_frame)
{
nlohmann::json serial = nlohmann::json({
int(Replay::time_precision * key_frame.time),
int(Replay::position_precision * key_frame.translation.x),
int(Replay::position_precision * key_frame.translation.y)});
if (key_frame.mirrored || key_frame.event != Replay::Event::none)
{
serial.push_back(int(key_frame.mirrored));
}
if (key_frame.event != Replay::Event::none)
{
serial.push_back(key_frame.event);
}
json = serial;
}
void cakefoot::from_json(const nlohmann::json& json, Replay::KeyFrame& key_frame)
{
float time {float(json.at(0)) / Replay::time_precision};
glm::vec2 position = glm::fvec2{json.at(1), json.at(2)} / glm::fvec2{Replay::position_precision};
if (json.size() == 3)
{
key_frame = Replay::KeyFrame{time, position};
}
else if (json.size() == 4)
{
key_frame = Replay::KeyFrame{time, position, bool(json.at(3).get<int>())};
}
else
{
key_frame = Replay::KeyFrame{time, position, bool(json.at(3).get<int>()), json.at(4)};
}
}