yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
graphics_scratch_asset.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_SCRATCH_ASSET_H_
2#define YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_SCRATCH_ASSET_H_
3
4#include <algorithm>
5#include <array>
6#include <cstdint>
7#include <set>
8#include <string>
9#include <vector>
10
11#include "absl/status/status.h"
12#include "absl/status/statusor.h"
14#include "nlohmann/json.hpp"
15
16namespace yaze::editor {
17
18// Detached pixels have no ROM address or loader ID. Applying an asset is a
19// separate, undoable sheet-store operation owned by GraphicsEditor.
21 uint64_t id = 0;
22 std::string name;
23 std::string source;
24 // Structured BIN provenance is independent of the human-readable source.
25 std::string source_path;
26 uint64_t source_offset = 0;
27 std::vector<uint8_t> original_chunk;
28 int bpp = 4;
29 std::vector<uint8_t> pixels = std::vector<uint8_t>(4096, 0);
30 std::array<uint16_t, 16> palette{};
31
32 absl::Status Validate() const {
33 if (name.empty() || name.size() > 256 || source.size() > 4096 ||
34 (bpp != 2 && bpp != 3 && bpp != 4) || pixels.size() != 4096) {
35 return absl::InvalidArgumentError(
36 "Invalid scratch asset metadata or dimensions");
37 }
38 if (source_path.size() > 4096 ||
39 source_path.find('\0') != std::string::npos ||
40 (source_path.empty() && (source_offset || !original_chunk.empty())) ||
41 (!source_path.empty() &&
42 original_chunk.size() != static_cast<size_t>(64 * 8 * bpp)))
43 return absl::InvalidArgumentError("Invalid scratch BIN provenance");
44 if (std::any_of(pixels.begin(), pixels.end(),
45 [this](uint8_t p) { return p >= (1 << bpp); }) ||
46 std::any_of(palette.begin(), palette.end(),
47 [](uint16_t c) { return c > 0x7fff; })) {
48 return absl::InvalidArgumentError(
49 "Scratch asset colors exceed the selected depth");
50 }
51 return absl::OkStatus();
52 }
53};
54
55inline absl::StatusOr<GraphicsScratchAsset> DecodeScratchBin(
56 const std::vector<uint8_t>& bytes, int bpp, std::string name) {
57 if ((bpp != 2 && bpp != 3 && bpp != 4) ||
58 bytes.size() != static_cast<size_t>(64 * 8 * bpp)) {
59 return absl::InvalidArgumentError(
60 "BIN must contain exactly 64 SNES tiles at the selected 2/3/4 bpp");
61 }
63 asset.name = std::move(name);
64 asset.bpp = bpp;
65 for (int tile = 0; tile < 64; ++tile) {
66 const auto decoded = gfx::UnpackBppTile(bytes, tile * 8 * bpp, bpp);
67 for (int y = 0; y < 8; ++y) {
68 std::copy_n(
69 decoded.data + y * 8, 8,
70 asset.pixels.begin() + ((tile / 16) * 8 + y) * 128 + (tile % 16) * 8);
71 }
72 }
73 return asset;
74}
75
76inline absl::StatusOr<std::vector<uint8_t>> EncodeScratchBin(
77 const GraphicsScratchAsset& asset) {
78 if (auto status = asset.Validate(); !status.ok())
79 return status;
80 std::vector<uint8_t> bytes;
81 bytes.reserve(64 * 8 * asset.bpp);
82 for (int tile = 0; tile < 64; ++tile) {
83 snes_tile8 decoded{};
84 for (int y = 0; y < 8; ++y) {
85 std::copy_n(
86 asset.pixels.begin() + ((tile / 16) * 8 + y) * 128 + (tile % 16) * 8,
87 8, decoded.data + y * 8);
88 }
89 auto packed = gfx::PackBppTile(decoded, asset.bpp);
90 bytes.insert(bytes.end(), packed.begin(), packed.end());
91 }
92 return bytes;
93}
94
96 static constexpr size_t kMaxAssets = 128;
97 uint64_t next_id = 1;
98 std::vector<GraphicsScratchAsset> assets;
99
100 absl::Status Add(GraphicsScratchAsset asset) {
101 if (auto status = asset.Validate(); !status.ok())
102 return status;
103 if (assets.size() >= kMaxAssets || next_id == UINT64_MAX)
104 return absl::ResourceExhaustedError("Scratch library is full");
105 asset.id = next_id++;
106 assets.push_back(std::move(asset));
107 return absl::OkStatus();
108 }
109
110 std::string Serialize() const {
111 nlohmann::json root = {{"version", 1},
112 {"next_id", next_id},
113 {"assets", nlohmann::json::array()}};
114 for (const auto& asset : assets) {
115 root["assets"].push_back({{"id", asset.id},
116 {"name", asset.name},
117 {"source", asset.source},
118 {"bpp", asset.bpp},
119 {"pixels", asset.pixels},
120 {"palette", asset.palette}});
121 if (!asset.source_path.empty())
122 root["assets"].back()["bin_source"] = {
123 {"path", asset.source_path},
124 {"offset", asset.source_offset},
125 {"original_chunk", asset.original_chunk}};
126 }
127 return root.dump();
128 }
129
130 static absl::StatusOr<GraphicsScratchLibrary> Parse(const std::string& text) {
131 if (text.size() > 4 * 1024 * 1024)
132 return absl::ResourceExhaustedError("Scratch library exceeds 4 MiB");
133 try {
134 const auto root = nlohmann::json::parse(text);
135 if (!root.is_object() || root.at("version") != 1 ||
136 !root.at("assets").is_array() ||
137 root.at("assets").size() > kMaxAssets)
138 return absl::InvalidArgumentError("Unsupported scratch library");
140 std::set<uint64_t> ids;
141 for (const auto& entry : root.at("assets")) {
142 if (!entry.is_object() ||
143 entry.size() != 6 + (entry.contains("bin_source") ? 1 : 0))
144 return absl::InvalidArgumentError("Invalid scratch asset fields");
146 const auto& pixels = entry.at("pixels");
147 const auto& palette = entry.at("palette");
148 if (!pixels.is_array() || pixels.size() != 4096 ||
149 !palette.is_array() || palette.size() != 16 ||
150 !entry.at("id").is_number_unsigned())
151 return absl::InvalidArgumentError("Invalid scratch asset shape");
152 asset.id = entry.at("id").get<uint64_t>();
153 asset.name = entry.at("name").get<std::string>();
154 asset.source = entry.at("source").get<std::string>();
155 if (!entry.at("bpp").is_number_integer() || entry.at("bpp") < 2 ||
156 entry.at("bpp") > 4)
157 return absl::InvalidArgumentError("Invalid scratch bit depth");
158 asset.bpp = entry.at("bpp").get<int>();
159 if (entry.contains("bin_source")) {
160 const auto& source = entry.at("bin_source");
161 if (!source.is_object() || source.size() != 3 ||
162 !source.at("offset").is_number_unsigned() ||
163 !source.at("original_chunk").is_array() ||
164 source.at("original_chunk").size() !=
165 static_cast<size_t>(64 * 8 * asset.bpp))
166 return absl::InvalidArgumentError("Invalid scratch BIN provenance");
167 asset.source_path = source.at("path").get<std::string>();
168 asset.source_offset = source.at("offset").get<uint64_t>();
169 if (asset.source_path.empty())
170 return absl::InvalidArgumentError("Empty scratch BIN source path");
171 for (const auto& byte : source.at("original_chunk")) {
172 if (!byte.is_number_integer() || byte < 0 || byte > 255)
173 return absl::InvalidArgumentError(
174 "Invalid scratch BIN source byte");
175 asset.original_chunk.push_back(byte.get<uint8_t>());
176 }
177 }
178 for (size_t i = 0; i < pixels.size(); ++i) {
179 if (!pixels[i].is_number_integer() || pixels[i].get<int64_t>() < 0 ||
180 pixels[i].get<int64_t>() > 15)
181 return absl::InvalidArgumentError("Invalid scratch pixel");
182 asset.pixels[i] = pixels[i].get<uint8_t>();
183 }
184 for (size_t i = 0; i < palette.size(); ++i) {
185 if (!palette[i].is_number_integer() ||
186 palette[i].get<int64_t>() < 0 ||
187 palette[i].get<int64_t>() > 0x7fff)
188 return absl::InvalidArgumentError("Invalid scratch palette");
189 asset.palette[i] = palette[i].get<uint16_t>();
190 }
191 if (auto status = asset.Validate(); !status.ok())
192 return status;
193 if (asset.id == 0 || asset.id == UINT64_MAX ||
194 !ids.insert(asset.id).second)
195 return absl::InvalidArgumentError(
196 "Invalid or duplicate scratch asset ID");
197 library.next_id = std::max(library.next_id, asset.id + 1);
198 library.assets.push_back(std::move(asset));
199 }
200 if (!root.at("next_id").is_number_unsigned())
201 return absl::InvalidArgumentError("Invalid scratch ID sequence");
202 library.next_id =
203 std::max(library.next_id, root.at("next_id").get<uint64_t>());
204 return library;
205 } catch (const nlohmann::json::exception&) {
206 return absl::InvalidArgumentError("Invalid scratch library JSON");
207 }
208 }
209};
210
211} // namespace yaze::editor
212#endif // YAZE_APP_EDITOR_GRAPHICS_GRAPHICS_SCRATCH_ASSET_H_
Editors are the view controllers for the application.
absl::StatusOr< GraphicsScratchAsset > DecodeScratchBin(const std::vector< uint8_t > &bytes, int bpp, std::string name)
absl::StatusOr< std::vector< uint8_t > > EncodeScratchBin(const GraphicsScratchAsset &asset)
snes_tile8 UnpackBppTile(std::span< const uint8_t > data, const uint32_t offset, const uint32_t bpp)
Definition snes_tile.cc:25
std::vector< uint8_t > PackBppTile(const snes_tile8 &tile, const uint32_t bpp)
Definition snes_tile.cc:74
8x8 SNES tile data
absl::Status Add(GraphicsScratchAsset asset)
static absl::StatusOr< GraphicsScratchLibrary > Parse(const std::string &text)
std::vector< GraphicsScratchAsset > assets