yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
scratch_workspace.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <fstream>
5#include <iterator>
6#include <limits>
7#include <set>
8#include <system_error>
9
10#include "absl/strings/str_cat.h"
11#include "core/project.h"
13#include "nlohmann/json.hpp"
14
15namespace yaze::editor {
16namespace {
17
18using Json = nlohmann::json;
19
20absl::Status InvalidScratch() {
21 return absl::DataLossError("Invalid scratch workspace data");
22}
23
24bool IsInteger(const Json& value, int low, int high) {
25 if (!value.is_number_integer())
26 return false;
27 if (value.is_number_unsigned()) {
28 const auto number = value.get<uint64_t>();
29 return (low <= 0 || number >= static_cast<uint64_t>(low)) &&
30 number <= static_cast<uint64_t>(high);
31 }
32 const auto number = value.get<int64_t>();
33 return number >= low && number <= high;
34}
35
36bool IsId(const Json& value) {
37 return value.is_number_unsigned() && value.get<uint64_t>() > 0 &&
38 value.get<uint64_t>() < std::numeric_limits<uint64_t>::max();
39}
40
41absl::StatusOr<ScratchPage> ParsePage(const Json& root, bool legacy) {
42 if (!root.is_object())
43 return InvalidScratch();
44 ScratchPage page;
45 if (!legacy) {
46 if (!root.contains("id") || !IsId(root["id"]))
47 return InvalidScratch();
48 page.id = root["id"].get<uint64_t>();
49 }
50 if (root.contains("name")) {
51 if (!root["name"].is_string())
52 return InvalidScratch();
53 page.name = root["name"].get<std::string>();
54 if (page.name.empty() || page.name.size() > 128)
55 return InvalidScratch();
56 }
57 for (const auto* key : {"width", "height"}) {
58 if (root.contains(key) && !IsInteger(root[key], 1, 32))
59 return InvalidScratch();
60 }
61 page.width = root.value("width", 16);
62 page.height = root.value("height", 16);
63 if (root.contains("in_use") && !root["in_use"].is_boolean())
64 return InvalidScratch();
65 page.in_use = root.value("in_use", false);
66 if (root.contains("source_map")) {
67 if (!IsInteger(root["source_map"], -1, 0xFFFF))
68 return InvalidScratch();
69 page.source_map = root["source_map"].get<int>();
70 }
71 if (!root.contains("tiles") || !root["tiles"].is_array() ||
72 root["tiles"].size() < static_cast<size_t>(page.height) ||
73 root["tiles"].size() > 32)
74 return InvalidScratch();
75 for (size_t y = 0; y < root["tiles"].size(); ++y) {
76 const auto& row = root["tiles"][y];
77 if (!row.is_array() || row.size() < static_cast<size_t>(page.width) ||
78 row.size() > 32)
79 return InvalidScratch();
80 for (size_t x = 0; x < row.size(); ++x) {
81 if (!IsInteger(row[x], 0, 0xFFFF))
82 return InvalidScratch();
83 page.tile_data[x][y] = row[x].get<int>();
84 }
85 }
86 return page;
87}
88
89absl::StatusOr<std::optional<std::string>> ReadContents(
90 const std::filesystem::path& path) {
91 std::error_code error;
92 const bool exists = std::filesystem::exists(path, error);
93 if (error)
94 return absl::InternalError(
95 absl::StrCat("Cannot inspect scratch workspace: ", error.message()));
96 if (!exists)
97 return std::optional<std::string>{};
98 if (!std::filesystem::is_regular_file(path, error) || error)
99 return absl::FailedPreconditionError(
100 "Scratch workspace is not a regular file");
101 const auto size = std::filesystem::file_size(path, error);
102 if (error || size > 1024 * 1024)
103 return absl::DataLossError("Scratch workspace exceeds the 1 MiB limit");
104 std::ifstream input(path, std::ios::binary);
105 if (!input)
106 return absl::PermissionDeniedError("Cannot read scratch workspace");
107 std::string contents((std::istreambuf_iterator<char>(input)), {});
108 if (input.bad())
109 return absl::InternalError("Failed to read scratch workspace");
110 return std::optional<std::string>(std::move(contents));
111}
112
113} // namespace
114
115bool ScratchPage::operator==(const ScratchPage& other) const {
116 return id == other.id && name == other.name && width == other.width &&
117 height == other.height && in_use == other.in_use &&
118 source_map == other.source_map && tile_data == other.tile_data;
119}
120
122 TileBrush brush;
123 brush.width = width;
124 brush.height = height;
125 brush.tile_ids.reserve(width * height);
126 for (int y = 0; y < height; ++y)
127 for (int x = 0; x < width; ++x)
128 brush.tile_ids.push_back(tile_data[x][y]);
129 return brush;
130}
131
133 const ScratchWorkspaceState& other) const {
134 return pages == other.pages && selected_id == other.selected_id &&
135 next_id == other.next_id;
136}
137
139 // Restoring an Undo snapshot must not recycle an ID already issued later.
140 const uint64_t next_id = std::max(state_.next_id, state.next_id);
141 state_ = state;
142 state_.next_id = next_id;
143}
144
146 return const_cast<ScratchPage&>(
147 static_cast<const ScratchWorkspace*>(this)->current());
148}
149
151 const auto it = std::find_if(
152 state_.pages.begin(), state_.pages.end(),
153 [&](const auto& page) { return page.id == state_.selected_id; });
154 return *it;
155}
156
157bool ScratchWorkspace::Select(uint64_t id) {
158 for (const auto& page : state_.pages) {
159 if (page.id == id) {
160 state_.selected_id = id;
161 return true;
162 }
163 }
164 return false;
165}
166
167absl::Status ScratchWorkspace::AddPage(bool duplicate) {
168 if (state_.pages.size() >= kMaxPages ||
169 state_.next_id >= std::numeric_limits<uint64_t>::max() - 1)
170 return absl::ResourceExhaustedError("Scratch workspace is full (32 pages)");
171 ScratchPage page = duplicate ? current() : ScratchPage{};
172 page.id = state_.next_id++;
173 page.name = duplicate ? page.name.substr(0, 123) + " copy"
174 : absl::StrCat("Scratch ", page.id);
175 state_.selected_id = page.id;
176 state_.pages.push_back(std::move(page));
177 return absl::OkStatus();
178}
179
180absl::Status ScratchWorkspace::Rename(absl::string_view name) {
181 if (name.empty() || name.size() > 128)
182 return absl::InvalidArgumentError(
183 "Scratch name must contain 1 to 128 bytes");
184 current().name = std::string(name);
185 return absl::OkStatus();
186}
187
189 if (state_.pages.size() <= 1)
190 return absl::FailedPreconditionError("Keep at least one scratch layout");
191 const uint64_t selected = state_.selected_id;
192 state_.pages.erase(std::remove_if(state_.pages.begin(), state_.pages.end(),
193 [selected](const auto& page) {
194 return page.id == selected;
195 }),
196 state_.pages.end());
197 state_.selected_id = state_.pages.front().id;
198 return absl::OkStatus();
199}
200
201absl::Status ScratchWorkspace::Resize(int width, int height) {
202 if (width < 1 || width > 32 || height < 1 || height > 32)
203 return absl::InvalidArgumentError(
204 "Scratch dimensions must be 1 to 32 tiles");
205 current().width = width;
206 current().height = height;
207 return absl::OkStatus();
208}
209
210absl::Status ScratchWorkspace::Stamp(const TileBrush& brush, int x, int y) {
211 if (!brush.valid())
212 return absl::InvalidArgumentError("Invalid scratch Tile16 brush");
213 if (x < 0 || y < 0 || x >= current().width || y >= current().height)
214 return absl::OutOfRangeError("Scratch stamp anchor is outside the page");
215 auto& page = current();
216 for (int sy = 0; sy < brush.height && y + sy < page.height; ++sy)
217 for (int sx = 0; sx < brush.width && x + sx < page.width; ++sx)
218 page.tile_data[x + sx][y + sy] = brush.at(sx, sy);
219 page.in_use = true;
220 return absl::OkStatus();
221}
222
223absl::Status ScratchWorkspace::Capture(const TileBrush& brush, int source_map) {
224 if (!brush.valid() || brush.width > 32 || brush.height > 32)
225 return absl::InvalidArgumentError(
226 "Capture a selection up to 32 by 32 tiles");
227 if (source_map < -1 || source_map > 0xFFFF)
228 return absl::InvalidArgumentError("Invalid scratch source map");
229 const auto status = AddPage();
230 if (!status.ok())
231 return status;
232 auto& page = current();
233 page.width = brush.width;
234 page.height = brush.height;
235 page.source_map = source_map;
236 page.name = absl::StrCat("Map ", source_map, " selection");
237 return Stamp(brush, 0, 0);
238}
239
241 auto& page = current();
242 page.in_use = false;
243 for (auto& column : page.tile_data)
244 column.fill(0);
245}
246
247absl::Status ScratchWorkspace::Parse(absl::string_view contents) {
248 if (contents.size() > 1024 * 1024)
249 return absl::DataLossError("Scratch workspace exceeds the 1 MiB limit");
250 const auto root =
251 Json::parse(contents.begin(), contents.end(), nullptr, false);
252 if (!root.is_object() || !root.contains("version") ||
253 !IsInteger(root["version"], 1, 2))
254 return InvalidScratch();
256 if (root["version"].get<int>() == 1) {
257 const auto page = ParsePage(root, true);
258 if (!page.ok())
259 return page.status();
260 parsed.pages = {*page};
261 } else {
262 if (!root.contains("pages") || !root["pages"].is_array() ||
263 root["pages"].empty() || root["pages"].size() > kMaxPages ||
264 !root.contains("selected_id") || !IsId(root["selected_id"]) ||
265 !root.contains("next_id") || !IsId(root["next_id"]))
266 return InvalidScratch();
267 parsed.pages.clear();
268 std::set<uint64_t> ids;
269 for (const auto& entry : root["pages"]) {
270 const auto page = ParsePage(entry, false);
271 if (!page.ok())
272 return page.status();
273 if (!ids.insert(page->id).second)
274 return InvalidScratch();
275 parsed.pages.push_back(*page);
276 }
277 parsed.selected_id = root["selected_id"].get<uint64_t>();
278 parsed.next_id = root["next_id"].get<uint64_t>();
279 if (!ids.count(parsed.selected_id) || parsed.next_id <= *ids.rbegin())
280 return InvalidScratch();
281 }
282 state_ = std::move(parsed);
283 return absl::OkStatus();
284}
285
286std::string ScratchWorkspace::Serialize() const {
287 Json root = {{"version", 2},
288 {"selected_id", state_.selected_id},
289 {"next_id", state_.next_id},
290 {"pages", Json::array()}};
291 for (const auto& page : state_.pages) {
292 Json entry = {{"id", page.id}, {"name", page.name},
293 {"width", page.width}, {"height", page.height},
294 {"in_use", page.in_use}, {"source_map", page.source_map},
295 {"tiles", Json::array()}};
296 // Preserve hidden cells when a page is temporarily shrunk.
297 for (int y = 0; y < 32; ++y) {
298 Json row = Json::array();
299 for (int x = 0; x < 32; ++x)
300 row.push_back(page.tile_data[x][y]);
301 entry["tiles"].push_back(std::move(row));
302 }
303 root["pages"].push_back(std::move(entry));
304 }
305 return root.dump(2) + '\n';
306}
307
308absl::Status ScratchWorkspace::Load(const std::filesystem::path& path) {
309 file_loaded_ = false;
310 const auto contents = ReadContents(path);
311 if (!contents.ok())
312 return contents.status();
313 if (*contents) {
314 const auto status = Parse(**contents);
315 if (!status.ok())
316 return status;
317 } else {
319 }
320 saved_contents_ = *contents;
321 file_loaded_ = true;
322 return absl::OkStatus();
323}
324
325absl::Status ScratchWorkspace::Save(const std::filesystem::path& path) {
326 if (!file_loaded_)
327 return absl::FailedPreconditionError(
328 "Scratch workspace was not loaded safely; preserve and repair its "
329 "file");
330#ifndef __EMSCRIPTEN__
332 {std::filesystem::absolute(path)});
333 if (!lock.ok())
334 return lock.status();
335#endif
336 const auto contents = ReadContents(path);
337 if (!contents.ok())
338 return contents.status();
339 if (*contents != saved_contents_)
340 return absl::AbortedError(
341 "Scratch workspace changed in another session; unsaved pages retained");
342 const std::string serialized = Serialize();
343#ifndef __EMSCRIPTEN__
344 const auto status = project::WriteProjectFileAtomically(
345 path.string(), serialized, contents->has_value());
346#else
347 // Emscripten's virtual filesystem has no native project-writer backend.
348 std::ofstream output(path, std::ios::binary | std::ios::trunc);
349 output << serialized;
350 output.flush();
351 const auto status =
352 output.good() ? absl::OkStatus()
353 : absl::InternalError("Cannot save scratch workspace");
354#endif
355 if (!status.ok())
356 return status;
357 saved_contents_ = serialized;
358 return absl::OkStatus();
359}
360
361} // namespace yaze::editor
static Json parse(const std::string &)
Definition json.h:36
static Json array()
Definition json.h:35
std::string dump(int=-1, char=' ', bool=false, int=0) const
Definition json.h:91
absl::Status Capture(const TileBrush &brush, int source_map)
absl::Status Load(const std::filesystem::path &path)
absl::Status Resize(int width, int height)
const ScratchWorkspaceState & state() const
absl::Status Stamp(const TileBrush &brush, int x, int y)
std::optional< std::string > saved_contents_
static constexpr size_t kMaxPages
void Restore(const ScratchWorkspaceState &state)
absl::Status Parse(absl::string_view contents)
absl::Status Rename(absl::string_view name)
absl::Status AddPage(bool duplicate=false)
absl::Status Save(const std::filesystem::path &path)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
bool IsInteger(const Json &value, int low, int high)
absl::StatusOr< std::optional< std::string > > ReadContents(const std::filesystem::path &path)
absl::StatusOr< ScratchPage > ParsePage(const Json &root, bool legacy)
Editors are the view controllers for the application.
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:453
std::array< std::array< int, 32 >, 32 > tile_data
bool operator==(const ScratchPage &other) const
bool operator==(const ScratchWorkspaceState &other) const
std::vector< ScratchPage > pages
int at(int x, int y) const
Definition tile_brush.h:24
std::vector< int > tile_ids
Definition tile_brush.h:15