Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
reaction_hot_reload.cpp
Go to the documentation of this file.
1// reaction_hot_reload.cpp — 反応表のホットリロード(ファイルの指紋 → 読み直し → 今の世界に当てられるか。T-0139・ADR-0047)。
3
4#include <algorithm>
5#include <format>
6#include <fstream>
7#include <iterator>
8#include <system_error>
9#include <utility>
10#include <vector>
11
13
14namespace bicameral::script {
15
16 namespace {
17
18 // FNV-1a(64bit)。指紋は同じ PC の中で比べるだけ(ファイルにも世界にも入らない)
19 constexpr uint64_t FNV_OFFSET = 0xcbf29ce484222325ULL;
20 constexpr uint64_t FNV_PRIME = 0x100000001b3ULL;
21
22 uint64_t MixBytes(uint64_t hash, std::string_view bytes) {
23 for (const char byte : bytes) {
24 hash ^= static_cast<uint8_t>(byte);
25 hash *= FNV_PRIME;
26 }
27
28 return hash;
29 }
30
31 // 中身の区切り(パスと中身が混ざって別のファイルの組と同じ指紋にならないように、長さも混ぜる)
32 uint64_t MixField(uint64_t hash, std::string_view bytes) {
33 hash = MixBytes(hash, std::to_string(bytes.size()));
34 hash = MixBytes(hash, ":");
35
36 return MixBytes(hash, bytes);
37 }
38
39 std::string ReadWhole(const fs::path& path, bool& ok) {
40 std::ifstream file(path, std::ios::binary);
41 ok = static_cast<bool>(file);
42 if (!ok)
43 return {};
44
45 return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
46 }
47
48 std::string NamesNotIn(const std::vector<std::string>& names, const std::vector<std::string>& other) {
49 std::string text;
50 for (size_t index = 1; index < names.size(); ++index) { // 0 は使わない物質
51 if (std::ranges::contains(other, names[index]))
52 continue;
53
54 text += (text.empty() ? "" : "・") + names[index];
55 }
56
57 return text.empty() ? "なし" : text;
58 }
59
60 } // namespace
61
62 uint64_t DirectoryFingerprint(const fs::path& root) {
63 std::error_code error;
64 if (!fs::is_directory(root, error))
65 return MixField(FNV_OFFSET, "フォルダが無い");
66
67 std::vector<std::pair<std::string, fs::path>> files;
68 for (fs::recursive_directory_iterator it(root, error), end; !error && it != end; it.increment(error)) {
69 std::error_code entryError;
70 if (!it->is_regular_file(entryError))
71 continue;
72
73 const std::u8string relative = it->path().lexically_relative(root).generic_u8string();
74 files.emplace_back(std::string(relative.begin(), relative.end()), it->path());
75 }
76
77 if (error)
78 return MixField(FNV_OFFSET, "フォルダを読めない");
79
80 std::ranges::sort(files);
81 uint64_t hash = FNV_OFFSET;
82 for (const auto& [relative, path] : files) {
83 bool ok = false;
84 const std::string contents = ReadWhole(path, ok);
85 hash = MixField(hash, relative);
86 hash = MixField(hash, ok ? contents : std::string("読めない"));
87 }
88
89 return hash;
90 }
91
92 std::expected<void, std::string> CheckHotReloadCompatible(const sim::BakedReactionTable& current,
93 const sim::BakedReactionTable& next) {
94 if (current.speciesNames != next.speciesNames) {
95 return std::unexpected(
96 std::format("物質の一覧が変わったので今の世界には当てられない(足した: {} / 消した: "
97 "{})。物質を足す・消すときは起動し直す",
98 NamesNotIn(next.speciesNames, current.speciesNames),
99 NamesNotIn(current.speciesNames, next.speciesNames)));
100 }
101
102 if (current.elementNames != next.elementNames || current.speciesElements != next.speciesElements) {
103 return std::unexpected(
104 "物質の元素の組み立て(または元素の一覧)が変わったので今の世界には当てられない("
105 "セルの元素が保存されなくなる)。起動し直す");
106 }
107
108 return {};
109 }
110
112 std::shared_ptr<const LoadedReactionTable> current)
113 : m_source(std::move(source)), m_current(std::move(current)) {
116 }
117
118 // パッケージのフォルダと型の定義(型の定義を直しても読み直す)
120 const fs::path root = m_source.packageRoot.empty() ? DefaultPackageRoot() : m_source.packageRoot;
121 const fs::path definitions = m_source.typeDefinitions.empty() ? DefaultTypeDefinitionsPath()
122 : m_source.typeDefinitions;
123 bool ok = false;
124 const std::string definitionText = ReadWhole(definitions, ok);
125
126 return MixField(DirectoryFingerprint(root), ok ? definitionText : std::string("型の定義を読めない"));
127 }
128
130 const uint64_t fingerprint = Fingerprint();
131 if (fingerprint == m_seen) {
132 m_pending = fingerprint;
133 return {};
134 }
135
136 // 書きかけ(保存の途中)を読まないよう、同じ指紋を 2 回続けて見てから読む
137 if (fingerprint != m_pending) {
138 m_pending = fingerprint;
139 return {.state = HotReloadState::Waiting};
140 }
141
142 m_seen = fingerprint;
143
144 return Reload();
145 }
146
148 auto loaded = LoadReactionTable(m_source);
149 if (!loaded)
150 return {.state = HotReloadState::Failed, .message = std::move(loaded.error())};
151
152 if (loaded->tableVersion == m_current->tableVersion)
153 return {}; // コメント・空白・並びだけの変更(同じ法則)
154
155 if (const auto compatible = CheckHotReloadCompatible(m_current->table, loaded->table); !compatible)
156 return {.state = HotReloadState::Failed, .message = compatible.error()};
157
158 std::string message;
159 for (const PackageRejection& rejection : loaded->rejected)
160 message += std::format("{}パッケージ {} を読まなかった: {}", message.empty() ? "" : " / ",
161 rejection.package, rejection.reason);
162
163 m_current = std::make_shared<const LoadedReactionTable>(std::move(*loaded));
164
165 return {.state = HotReloadState::Reloaded, .table = m_current, .message = std::move(message)};
166 }
167
168} // namespace bicameral::script
ReactionTableHotReload(ReactionTableSource source, std::shared_ptr< const LoadedReactionTable > current)
std::shared_ptr< const LoadedReactionTable > m_current
uint64_t MixBytes(uint64_t hash, std::string_view bytes)
uint64_t MixField(uint64_t hash, std::string_view bytes)
std::string NamesNotIn(const std::vector< std::string > &names, const std::vector< std::string > &other)
uint64_t DirectoryFingerprint(const fs::path &root)
std::expected< void, std::string > CheckHotReloadCompatible(const sim::BakedReactionTable &current, const sim::BakedReactionTable &next)
fs::path DefaultTypeDefinitionsPath()
std::expected< LoadedReactionTable, std::string > LoadReactionTable(const ReactionTableSource &source)
std::vector< std::string > speciesNames
std::vector< uint32_t > speciesElements
std::vector< std::string > elementNames