Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
replay_file.cpp
Go to the documentation of this file.
1// replay_file.cpp — 再生ファイルの読み書き。形と約束は replay_file.h。
2#include "save/replay_file.h"
3
4#include <array>
5#include <bit>
6#include <cstring>
7#include <format>
8#include <fstream>
9
10#include "core/aliases.h"
11#include "core/unicode.h"
12
13namespace bicameral::save {
14 namespace {
15
16 static_assert(std::endian::native == std::endian::little, "再生ファイルはリトルエンディアンで書く");
17
18 constexpr std::array<char, 4> MAGIC = {'B', 'C', 'R', 'P'};
19 constexpr uint64_t HEADER_BYTES = 64;
20 constexpr uint64_t HASH_BYTES = 16;
21 constexpr uint64_t ALIGNMENT = 8;
22 // 壊れたファイルで巨大な確保をしないための上限(1 GiB)
23 constexpr uint64_t MAX_FILE_BYTES = uint64_t{1} << 30;
24
25 // ファイルの先頭 64 バイト(replay_file.h の表)
26 // ファイルの先頭 64 バイト(並びは replay_file.h の先頭の表)
27 struct Header {
28 // --- 形の確認 ---
29 std::array<char, 4> magic = MAGIC; // "BCRP"
30 uint32_t formatVersion = REPLAY_FORMAT_VERSION; // 形式の版
31
32 // --- どの世界の、どの状態から始まるか ---
33 uint32_t world = 0; // ReplayWorld
34 uint32_t reserved = 0; // 0(後ろの u64 を 8 バイトの境界に揃える)
35 uint64_t tableVersion = 0; // 反応表などの版
36 uint64_t seed = 0; // 世界の生成のシード
37 uint64_t startTick = 0; // 初期状態の刻み
38
39 // --- 後ろに続くものの大きさ ---
40 uint64_t deltaBytes = 0; // 初期状態の差分のバイト数
41 uint64_t commandCount = 0; // コマンドの数
42 uint64_t hashCount = 0; // ハッシュの数
43 };
44
45 static_assert(sizeof(Header) == HEADER_BYTES);
46
47 constexpr uint64_t AlignUp(uint64_t value) {
48 return (value + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
49 }
50
51 // 見出しの数から全体の大きさ(桁あふれしないように、上限で先に切る)
52 std::expected<uint64_t, std::string> ExpectedBytes(const Header& header) {
55 return std::unexpected("再生ファイルの見出しの数が大きすぎる");
56 }
57
59 header.hashCount * HASH_BYTES;
60 }
61
62 // 並びの約束: コマンドは (targetTick, sequence) の昇順、ハッシュは刻みの昇順。どちらも初期状態の刻みから
63 std::expected<void, std::string> CheckOrder(const ReplayFile& replay) {
64 for (size_t index = 0; index < replay.commands.size(); ++index) {
65 const sim::Command& command = replay.commands[index];
66 if (command.targetTick < replay.startTick) {
67 return std::unexpected(std::format("コマンド {} の刻み {} が初期状態の刻み {} より前", index,
68 command.targetTick, replay.startTick));
69 }
70
71 if (index > 0 && !sim::CommandPrecedes(replay.commands[index - 1], command))
72 return std::unexpected(std::format("コマンド {} が (刻み, 番号) の昇順になっていない", index));
73 }
74
75 for (size_t index = 1; index < replay.tickHashes.size(); ++index) {
76 if (replay.tickHashes[index - 1].tick >= replay.tickHashes[index].tick)
77 return std::unexpected(std::format("ハッシュ {} が刻みの昇順になっていない", index));
78 }
79
80 return {};
81 }
82
83 // ログ用のパス(fs::path::string は日本語で例外になりうるので UTF-16 から変換する)
84 std::string PathText(const fs::path& path) {
85 return ToUtf8(path.wstring());
86 }
87
88 void Append(std::vector<std::byte>& bytes, std::span<const std::byte> source) {
89 bytes.insert(bytes.end(), source.begin(), source.end());
90 }
91
92 // offset から values の大きさだけ写す(大きさは呼ぶ前に確かめてある)
93 template <typename T>
94 void CopyOut(std::span<const std::byte> bytes, uint64_t offset, std::span<T> values) {
95 if (values.empty())
96 return;
97
98 std::memcpy(values.data(), bytes.data() + offset, values.size_bytes());
99 }
100
101 } // namespace
102
103 // --- 書く ---
104
105 std::expected<std::vector<std::byte>, std::string> SerializeReplay(const ReplayFile& replay) {
106 if (auto order = CheckOrder(replay); !order)
107 return std::unexpected(order.error());
108
109 const Header header{.world = static_cast<uint32_t>(replay.world),
110 .tableVersion = replay.tableVersion,
111 .seed = replay.seed,
112 .startTick = replay.startTick,
113 .deltaBytes = replay.initialDelta.size(),
114 .commandCount = replay.commands.size(),
115 .hashCount = replay.tickHashes.size()};
116
117 const auto total = ExpectedBytes(header);
118 if (!total)
119 return std::unexpected(total.error());
120
121 std::vector<std::byte> bytes;
122 bytes.reserve(*total);
123 Append(bytes, std::as_bytes(std::span(&header, 1)));
124 Append(bytes, replay.initialDelta);
125 bytes.resize(HEADER_BYTES + AlignUp(replay.initialDelta.size()), std::byte{0});
126 Append(bytes, std::as_bytes(std::span(replay.commands)));
127
128 for (const ReplayTickHash& entry : replay.tickHashes) {
129 const std::array<uint64_t, 2> words = {entry.tick, entry.hash};
130 Append(bytes, std::as_bytes(std::span(words)));
131 }
132
133 return bytes;
134 }
135
136 std::expected<void, std::string> WriteReplayFile(const fs::path& path, const ReplayFile& replay) {
137 const auto bytes = SerializeReplay(replay);
138 if (!bytes)
139 return std::unexpected(bytes.error());
140
141 std::ofstream file(path, std::ios::binary | std::ios::trunc);
142 file.write(reinterpret_cast<const char*>(bytes->data()), static_cast<std::streamsize>(bytes->size()));
143 if (!file)
144 return std::unexpected(std::format("再生ファイルを書けない: {}", PathText(path)));
145
146 return {};
147 }
148
149 // --- 読む ---
150
151 std::expected<ReplayFile, std::string> ParseReplay(std::span<const std::byte> bytes) {
152 Header header;
153 if (bytes.size() < HEADER_BYTES)
154 return std::unexpected("再生ファイルが見出しより短い");
155
156 std::memcpy(&header, bytes.data(), HEADER_BYTES);
157 if (header.magic != MAGIC)
158 return std::unexpected("再生ファイルではない(先頭が BCRP でない)");
159
160 if (header.formatVersion != REPLAY_FORMAT_VERSION) {
161 return std::unexpected(std::format("再生ファイルの形式の版 {} は読めない(読めるのは {})",
162 header.formatVersion, REPLAY_FORMAT_VERSION));
163 }
164
165 if (header.world != static_cast<uint32_t>(ReplayWorld::Probe))
166 return std::unexpected(std::format("知らない世界の種類 {}", header.world));
167
168 const auto total = ExpectedBytes(header);
169 if (!total)
170 return std::unexpected(total.error());
171
172 if (*total != bytes.size())
173 return std::unexpected(
174 std::format("再生ファイルの大きさが合わない: 見出しから {} 実際 {}", *total, bytes.size()));
175
176 ReplayFile replay{.world = static_cast<ReplayWorld>(header.world),
177 .tableVersion = header.tableVersion,
178 .seed = header.seed,
179 .startTick = header.startTick};
180
181 replay.initialDelta.resize(header.deltaBytes);
182 replay.commands.resize(header.commandCount);
183 std::vector<std::array<uint64_t, 2>> hashes(header.hashCount);
184 uint64_t offset = HEADER_BYTES;
185 CopyOut(bytes, offset, std::span(replay.initialDelta));
186 offset += AlignUp(header.deltaBytes);
187 CopyOut(bytes, offset, std::span(replay.commands));
188 offset += header.commandCount * sim::COMMAND_BYTES;
189 CopyOut(bytes, offset, std::span(hashes));
190 replay.tickHashes.reserve(hashes.size());
191 for (const auto& [tick, hash] : hashes)
192 replay.tickHashes.push_back({.tick = tick, .hash = hash});
193
194 if (auto order = CheckOrder(replay); !order)
195 return std::unexpected(order.error());
196
197 return replay;
198 }
199
200 std::expected<ReplayFile, std::string> ReadReplayFile(const fs::path& path) {
201 std::ifstream file(path, std::ios::binary | std::ios::ate);
202 if (!file)
203 return std::unexpected(std::format("再生ファイルを開けない: {}", PathText(path)));
204
205 const std::streamoff size = file.tellg();
206 if (size < 0 || static_cast<uint64_t>(size) > MAX_FILE_BYTES)
207 return std::unexpected(std::format("再生ファイルの大きさが扱えない: {}", PathText(path)));
208
209 std::vector<std::byte> bytes(static_cast<size_t>(size));
210 file.seekg(0);
211 file.read(reinterpret_cast<char*>(bytes.data()), size);
212 if (!file)
213 return std::unexpected(std::format("再生ファイルを読めない: {}", PathText(path)));
214
215 return ParseReplay(bytes);
216 }
217
218} // namespace bicameral::save
std::expected< uint64_t, std::string > ExpectedBytes(const Header &header)
void Append(std::vector< std::byte > &bytes, std::span< const std::byte > source)
void CopyOut(std::span< const std::byte > bytes, uint64_t offset, std::span< T > values)
std::expected< void, std::string > CheckOrder(const ReplayFile &replay)
std::expected< ReplayFile, std::string > ReadReplayFile(const fs::path &path)
std::expected< std::vector< std::byte >, std::string > SerializeReplay(const ReplayFile &replay)
std::expected< void, std::string > WriteReplayFile(const fs::path &path, const ReplayFile &replay)
constexpr uint32_t REPLAY_FORMAT_VERSION
Definition replay_file.h:30
std::expected< ReplayFile, std::string > ParseReplay(std::span< const std::byte > bytes)
constexpr bool CommandPrecedes(const Command &a, const Command &b)
Definition command.h:32
constexpr uint32_t COMMAND_BYTES
Definition command.h:16
std::string ToUtf8(std::wstring_view text)
Definition unicode.cpp:10
std::vector< sim::Command > commands
Definition replay_file.h:53
std::vector< std::byte > initialDelta
Definition replay_file.h:50
std::vector< ReplayTickHash > tickHashes
Definition replay_file.h:54