• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JackAshwell11 / Hades / 17012280069

16 Aug 2025 07:51PM UTC coverage: 93.521% (-0.9%) from 94.372%
17012280069

Pull #348

github

web-flow
Merge 356dc6e13 into 883c3507a
Pull Request #348: Implement a save system allowing the player to save, load, delete, and start new games

618 of 676 new or added lines in 34 files covered. (91.42%)

2 existing lines in 1 file now uncovered.

2165 of 2315 relevant lines covered (93.52%)

20587.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.33
/src/hades_extensions/src/save_manager.cpp
1
// Related header
2
#include "save_manager.hpp"
3

4
// Std headers
5
#include <chrono>
6
#include <filesystem>
7
#include <format>
8
#include <fstream>
9

10
// External headers
11
#include <nlohmann/json.hpp>
12

13
// Local headers
14
#include "ecs/registry.hpp"
15
#include "events.hpp"
16
#include "game_state.hpp"
17

18
namespace {
19
/// The name of the save files.
20
inline auto SAVE_FILE_NAME{"autosave"};
21

22
/// The maximum number of save files to keep.
23
constexpr size_t MAX_SAVE_FILES{20};
24

25
/// Generate a unique save file name.
26
///
27
/// @return A unique save file name.
28
auto generate_save_file_name() -> std::string {
61✔
29
  static std::mt19937_64 gen(std::random_device{}());
61✔
30
  return std::format("{}_{:016x}.json", SAVE_FILE_NAME, gen());
61✔
31
}
32
}  // namespace
33

34
SaveManager::SaveManager(const std::shared_ptr<Registry> &registry, const std::shared_ptr<GameState> &game_state)
23✔
35
    : registry_(registry), game_state_(game_state) {}
23✔
36

37
void SaveManager::set_save_path(const std::string &path) {
14✔
38
  save_path_ = path;
14✔
39
  refresh_save_files();
14✔
40
}
13✔
41

42
void SaveManager::new_game() const { game_state_->reset_level(LevelType::Lobby); }
1✔
43

44
void SaveManager::load_save(const int save_index) const {
3✔
45
  const std::string file_path{save_files_.at(save_index).path};
3✔
46
  std::ifstream stream{file_path};
2✔
47
  if (!stream.is_open()) {
2✔
NEW
48
    throw std::runtime_error("Could not open save file: " + file_path);
×
49
  }
50

51
  nlohmann::json json_data;
2✔
52
  stream >> json_data;
2✔
53
  for (const auto &component : registry_->get_game_object_components(game_state_->get_player_id())) {
15✔
54
    component->from_file(json_data);
14✔
55
    component->from_file(json_data, registry_.get());
14✔
56
  }
1✔
57
  game_state_->reset_level(json_data.at("dungeon_level").get<LevelType>());
1✔
58
}
5✔
59

60
void SaveManager::save_game() {
61✔
61
  const std::filesystem::path file_path{save_path_ / generate_save_file_name()};
61✔
62
  std::ofstream stream{file_path};
61✔
63
  if (!stream.is_open()) {
61✔
NEW
64
    throw std::runtime_error("Could not open save file: " + file_path.string());
×
65
  }
66

67
  try {
68
    nlohmann::json json_data;
61✔
69
    const std::chrono::zoned_time local_time{std::chrono::current_zone(), std::chrono::system_clock::now()};
61✔
70
    json_data["save_date"] = std::format("{0:%FT%T%z}", local_time);
61✔
71
    for (const auto &component : registry_->get_game_object_components(game_state_->get_player_id())) {
915✔
72
      component->to_file(json_data);
854✔
73
      component->to_file(json_data, registry_.get());
854✔
74
    }
61✔
75
    json_data["dungeon_level"] = game_state_->get_dungeon_level();
61✔
76
    stream << json_data.dump(4);
61✔
77
    stream.close();
61✔
78
    refresh_save_files();
61✔
79
  } catch (...) {
61✔
NEW
80
    stream.close();
×
NEW
81
    std::filesystem::remove(file_path);
×
NEW
82
    throw;
×
NEW
83
  }
×
84
}
122✔
85

86
void SaveManager::delete_save(const int save_index) {
2✔
87
  if (save_index < 0 || std::cmp_greater_equal(save_index, save_files_.size())) {
2✔
88
    throw std::out_of_range("Invalid save index: " + std::to_string(save_index));
1✔
89
  }
90
  if (const std::string file_path{save_files_.at(save_index).path}; std::filesystem::remove(file_path)) {
1✔
91
    refresh_save_files();
1✔
92
  } else {
NEW
93
    throw std::runtime_error("Could not delete save file: " + file_path);
×
94
  }
1✔
95
}
1✔
96

97
void SaveManager::refresh_save_files() {
76✔
98
  save_files_.clear();
76✔
99
  for (const auto &entry : std::filesystem::directory_iterator(save_path_)) {
1,800✔
100
    if (!entry.is_regular_file() || entry.path().extension() != ".json") {
862✔
NEW
101
      continue;
×
102
    }
103
    std::ifstream file_stream(entry.path());
862✔
104
    nlohmann::json json_data;
862✔
105
    file_stream >> json_data;
862✔
106

107
    const SaveFileInfo info{.name = entry.path().stem().string(),
2,586✔
108
                            .path = entry.path().string(),
862✔
109
                            .last_modified = json_data.at("save_date").get<std::string>(),
1,724✔
110
                            .player_level = json_data.at("player_level").get<int>()};
2,586✔
111
    save_files_.push_back(info);
862✔
112
  }
937✔
113

114
  std::ranges::sort(save_files_.begin(), save_files_.end(), [](const SaveFileInfo &lhs, const SaveFileInfo &rhs) {
75✔
115
    return lhs.last_modified > rhs.last_modified;
4,116✔
116
  });
117
  if (save_files_.size() > MAX_SAVE_FILES) {
75✔
118
    for (auto i{MAX_SAVE_FILES}; i < save_files_.size(); i++) {
60✔
119
      std::filesystem::remove(save_files_[i].path);
30✔
120
    }
121
    save_files_.resize(MAX_SAVE_FILES);
30✔
122
  }
123
  notify<EventType::SaveFilesUpdated>(save_files_);
75✔
124
}
75✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc