10#include "absl/strings/str_cat.h"
13#include "nlohmann/json.hpp"
18using Json = nlohmann::json;
21 return absl::DataLossError(
"Invalid scratch workspace data");
25 if (!value.is_number_integer())
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);
32 const auto number = value.get<int64_t>();
33 return number >= low && number <= high;
37 return value.is_number_unsigned() && value.get<uint64_t>() > 0 &&
38 value.get<uint64_t>() < std::numeric_limits<uint64_t>::max();
42 if (!root.is_object())
46 if (!root.contains(
"id") || !
IsId(root[
"id"]))
48 page.
id = root[
"id"].get<uint64_t>();
50 if (root.contains(
"name")) {
51 if (!root[
"name"].is_string())
53 page.
name = root[
"name"].get<std::string>();
54 if (page.
name.empty() || page.
name.size() > 128)
57 for (
const auto* key : {
"width",
"height"}) {
58 if (root.contains(key) && !
IsInteger(root[key], 1, 32))
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())
65 page.
in_use = root.value(
"in_use",
false);
66 if (root.contains(
"source_map")) {
67 if (!
IsInteger(root[
"source_map"], -1, 0xFFFF))
69 page.
source_map = root[
"source_map"].get<
int>();
71 if (!root.contains(
"tiles") || !root[
"tiles"].is_array() ||
72 root[
"tiles"].size() <
static_cast<size_t>(page.
height) ||
73 root[
"tiles"].size() > 32)
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) ||
80 for (
size_t x = 0; x < row.size(); ++x) {
90 const std::filesystem::path& path) {
91 std::error_code error;
92 const bool exists = std::filesystem::exists(path, error);
94 return absl::InternalError(
95 absl::StrCat(
"Cannot inspect scratch workspace: ", error.message()));
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);
106 return absl::PermissionDeniedError(
"Cannot read scratch workspace");
107 std::string contents((std::istreambuf_iterator<char>(input)), {});
109 return absl::InternalError(
"Failed to read scratch workspace");
110 return std::optional<std::string>(std::move(contents));
126 for (
int y = 0; y <
height; ++y)
127 for (
int x = 0; x <
width; ++x)
151 const auto it = std::find_if(
153 [&](
const auto& page) { return page.id == state_.selected_id; });
170 return absl::ResourceExhaustedError(
"Scratch workspace is full (32 pages)");
173 page.
name = duplicate ? page.
name.substr(0, 123) +
" copy"
174 : absl::StrCat(
"Scratch ", page.
id);
177 return absl::OkStatus();
181 if (name.empty() || name.size() > 128)
182 return absl::InvalidArgumentError(
183 "Scratch name must contain 1 to 128 bytes");
185 return absl::OkStatus();
190 return absl::FailedPreconditionError(
"Keep at least one scratch layout");
193 [selected](
const auto& page) {
194 return page.id == selected;
198 return absl::OkStatus();
202 if (width < 1 || width > 32 || height < 1 || height > 32)
203 return absl::InvalidArgumentError(
204 "Scratch dimensions must be 1 to 32 tiles");
207 return absl::OkStatus();
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");
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);
220 return absl::OkStatus();
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");
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);
243 for (
auto& column : page.tile_data)
248 if (contents.size() > 1024 * 1024)
249 return absl::DataLossError(
"Scratch workspace exceeds the 1 MiB limit");
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);
259 return page.status();
260 parsed.
pages = {*page};
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);
272 return page.status();
273 if (!ids.insert(page->id).second)
274 return InvalidScratch();
275 parsed.
pages.push_back(*page);
277 parsed.
selected_id = root[
"selected_id"].get<uint64_t>();
278 parsed.
next_id = root[
"next_id"].get<uint64_t>();
280 return InvalidScratch();
282 state_ = std::move(parsed);
283 return absl::OkStatus();
287 Json root = {{
"version", 2},
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},
297 for (
int y = 0; y < 32; ++y) {
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));
303 root[
"pages"].push_back(std::move(entry));
305 return root.
dump(2) +
'\n';
310 const auto contents = ReadContents(path);
312 return contents.status();
314 const auto status =
Parse(**contents);
322 return absl::OkStatus();
327 return absl::FailedPreconditionError(
328 "Scratch workspace was not loaded safely; preserve and repair its "
330#ifndef __EMSCRIPTEN__
332 {std::filesystem::absolute(path)});
334 return lock.status();
336 const auto contents = ReadContents(path);
338 return contents.status();
340 return absl::AbortedError(
341 "Scratch workspace changed in another session; unsaved pages retained");
342 const std::string serialized =
Serialize();
343#ifndef __EMSCRIPTEN__
345 path.string(), serialized, contents->has_value());
348 std::ofstream output(path, std::ios::binary | std::ios::trunc);
349 output << serialized;
352 output.good() ? absl::OkStatus()
353 : absl::InternalError(
"Cannot save scratch workspace");
358 return absl::OkStatus();
static Json parse(const std::string &)
std::string dump(int=-1, char=' ', bool=false, int=0) const
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)
absl::Status DeletePage()
ScratchWorkspaceState state_
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)
std::string Serialize() const
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 IsId(const Json &value)
bool IsInteger(const Json &value, int low, int high)
absl::StatusOr< std::optional< std::string > > ReadContents(const std::filesystem::path &path)
absl::Status InvalidScratch()
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)
TileBrush AsBrush() const
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
std::vector< int > tile_ids