yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_graphics_preview.h
Go to the documentation of this file.
1#ifndef YAZE_CORE_OVERWORLD_GRAPHICS_PREVIEW_H_
2#define YAZE_CORE_OVERWORLD_GRAPHICS_PREVIEW_H_
3
4#include <algorithm>
5#include <cctype>
6#include <cstdint>
7#include <filesystem>
8#include <fstream>
9#include <limits>
10#include <map>
11#include <set>
12#include <span>
13#include <string>
14#include <utility>
15#include <vector>
16
17#include "absl/status/status.h"
18#include "absl/status/statusor.h"
20#include "nlohmann/json.hpp"
22
23namespace yaze::core {
24namespace graphics_preview_internal {
25
26inline absl::StatusOr<int> NonnegativeInt(const nlohmann::json& value,
27 const std::string& field) {
28 if (!value.is_number_integer() ||
29 (value.is_number_unsigned()
30 ? value.get<uint64_t>() > std::numeric_limits<int>::max()
31 : value.get<int64_t>() < 0 ||
32 value.get<int64_t>() > std::numeric_limits<int>::max())) {
33 return absl::InvalidArgumentError("Invalid graphics preview " + field);
34 }
35 return value.get<int>();
36}
37
38inline absl::StatusOr<zelda3::OverworldGraphicsPreview> LoadVersion2(
39 const nlohmann::json& document, const std::string& filename) {
40 if (!document.at("resources").is_object() ||
41 !document.at("profiles").is_object() ||
42 !document.at("bindings").is_array()) {
43 return absl::InvalidArgumentError("Invalid graphics preview catalog");
44 }
45 std::map<std::string, std::vector<uint8_t>> resources;
46 for (const auto& [id, resource] : document.at("resources").items()) {
47 if (id.empty() || resource.at("format") != "snes4bpp") {
48 return absl::InvalidArgumentError(
49 "Graphics preview resource needs an ID and snes4bpp format");
50 }
51 auto count =
52 NonnegativeInt(resource.at("byte_count"), "resource byte_count");
53 if (!count.ok())
54 return count.status();
55 if (*count == 0 || *count > 0x8000 || *count % 32 != 0) {
56 return absl::InvalidArgumentError(
57 "Invalid graphics preview resource size");
58 }
59 auto expected = resource.at("sha256").get<std::string>();
60 if (expected.size() != 64 ||
61 !std::all_of(expected.begin(), expected.end(),
62 [](unsigned char c) { return std::isxdigit(c); })) {
63 return absl::InvalidArgumentError(
64 "Invalid graphics preview resource SHA256");
65 }
66 std::transform(expected.begin(), expected.end(), expected.begin(),
67 [](unsigned char c) { return std::tolower(c); });
68 const auto asset_path = resource.at("path").get<std::string>();
69 if (asset_path.empty()) {
70 return absl::InvalidArgumentError(
71 "Graphics preview resource path is empty");
72 }
73 const auto path =
74 std::filesystem::path(filename).parent_path() / asset_path;
75 std::error_code ec;
76 const auto size = std::filesystem::file_size(path, ec);
77 if (ec) {
78 return absl::NotFoundError("Missing graphics preview asset: " +
79 path.string());
80 }
81 if (size != static_cast<size_t>(*count)) {
82 return absl::InvalidArgumentError(
83 "Graphics preview asset has wrong byte count: " + path.string());
84 }
85 std::vector<uint8_t> bytes(*count);
86 std::ifstream asset(path, std::ios::binary);
87 if (!asset.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
88 return absl::DataLossError("Cannot read graphics preview asset: " +
89 path.string());
90 }
92 {reinterpret_cast<const char*>(bytes.data()), bytes.size()}) !=
93 expected) {
94 return absl::DataLossError("Graphics preview resource SHA256 mismatch: " +
95 id);
96 }
97 resources.emplace(id, std::move(bytes));
98 }
99 std::map<std::string, zelda3::AreaGraphicsPreview> profiles;
100 for (const auto& [id, profile] : document.at("profiles").items()) {
101 if (id.empty() || !profile.at("transfers").is_array() ||
102 profile.at("transfers").empty()) {
103 return absl::InvalidArgumentError(
104 "Graphics preview profile needs transfers");
105 }
106 auto base = NonnegativeInt(profile.at("bg_character_base_word"), "BG base");
107 if (!base.ok())
108 return base.status();
109 auto& tiles = profiles[id];
110 for (const auto& transfer : profile.at("transfers")) {
111 const auto resource_id = transfer.at("resource_id").get<std::string>();
112 const auto resource = resources.find(resource_id);
113 if (resource == resources.end()) {
114 return absl::InvalidArgumentError(
115 "Unknown graphics preview resource: " + resource_id);
116 }
117 auto offset = NonnegativeInt(
118 transfer.value("source_offset", nlohmann::json(0)), "source_offset");
119 auto count =
120 NonnegativeInt(transfer.at("byte_count"), "transfer byte_count");
121 auto destination =
122 NonnegativeInt(transfer.at("vram_word_address"), "VRAM address");
123 if (!offset.ok())
124 return offset.status();
125 if (!count.ok())
126 return count.status();
127 if (!destination.ok())
128 return destination.status();
129 if (*offset % 32 != 0 || *count == 0 || *count % 32 != 0 ||
130 static_cast<size_t>(*offset) > resource->second.size() ||
131 static_cast<size_t>(*count) > resource->second.size() - *offset) {
132 return absl::InvalidArgumentError(
133 "Invalid graphics preview resource slice");
134 }
136 std::span<const uint8_t>(resource->second).subspan(*offset, *count),
137 *destination, *base);
138 if (!decoded.ok())
139 return decoded.status();
140 for (const auto& [character, pixels] : *decoded) {
141 if (!tiles.emplace(character, pixels).second) {
142 return absl::InvalidArgumentError(
143 "Overlapping graphics preview transfers");
144 }
145 }
146 }
147 }
149 for (const auto& binding : document.at("bindings")) {
150 const auto profile_id = binding.at("profile_id").get<std::string>();
151 const auto profile = profiles.find(profile_id);
152 if (profile == profiles.end()) {
153 return absl::InvalidArgumentError("Unknown graphics preview profile: " +
154 profile_id);
155 }
156 if (!binding.at("parent_maps").is_array() ||
157 binding.at("parent_maps").empty()) {
158 return absl::InvalidArgumentError(
159 "Graphics preview binding needs parent_maps");
160 }
161 for (const auto& value : binding.at("parent_maps")) {
162 auto parent = NonnegativeInt(value, "parent_map");
163 if (!parent.ok())
164 return parent.status();
165 if (*parent >= 160 || !result.emplace(*parent, profile->second).second) {
166 return absl::InvalidArgumentError(
167 "Invalid or duplicate graphics preview parent_map");
168 }
169 }
170 }
171 return result;
172}
173
174} // namespace graphics_preview_internal
175
176// Explicit project-owned, read-only rendering configuration. Assets resolve
177// relative to this file, never by ROM filename or a global area exception.
178// Version1 embeds area transfers. Version2 shares SHA256-checked resources and
179// sliced transfer profiles, then binds each profile to disjoint parent maps.
180inline absl::StatusOr<zelda3::OverworldGraphicsPreview>
181LoadOverworldGraphicsPreview(const std::string& filename) {
183 if (filename.empty())
184 return result;
185 std::ifstream input(filename);
186 if (!input)
187 return absl::NotFoundError("Cannot open graphics preview: " + filename);
188 try {
189 // Object keys are identities in version2. The ordinary JSON parser would
190 // silently accept the last duplicate resource/profile definition.
191 bool duplicate_key = false;
192 std::vector<std::set<std::string>> object_keys;
193 const auto document = nlohmann::json::parse(
194 input,
195 [&duplicate_key, &object_keys](int, nlohmann::json::parse_event_t event,
196 nlohmann::json& parsed) {
197 if (event == nlohmann::json::parse_event_t::object_start) {
198 object_keys.emplace_back();
199 } else if (event == nlohmann::json::parse_event_t::key) {
200 if (!object_keys.back().insert(parsed.get<std::string>()).second)
201 duplicate_key = true;
202 } else if (event == nlohmann::json::parse_event_t::object_end) {
203 object_keys.pop_back();
204 }
205 return true;
206 });
207 if (duplicate_key) {
208 return absl::InvalidArgumentError("Duplicate graphics preview JSON key");
209 }
211 document.at("schema_version"), "schema_version");
212 if (!version.ok())
213 return version.status();
214 if (*version == 2) {
215 return graphics_preview_internal::LoadVersion2(document, filename);
216 }
217 if (*version != 1 || !document.at("areas").is_array()) {
218 return absl::InvalidArgumentError("Unsupported graphics preview schema");
219 }
220 for (const auto& area : document.at("areas")) {
221 const int parent = area.at("parent_map").get<int>();
222 if (parent < 0 || parent >= 160 || result.contains(parent)) {
223 return absl::InvalidArgumentError(
224 "Invalid or duplicate graphics preview parent_map");
225 }
226 const int base = area.at("bg_character_base_word").get<int>();
227 auto& tiles = result[parent];
228 if (!area.at("transfers").is_array() || area.at("transfers").empty()) {
229 return absl::InvalidArgumentError("Graphics preview needs transfers");
230 }
231 for (const auto& transfer : area.at("transfers")) {
232 if (transfer.at("format").get<std::string>() != "snes4bpp") {
233 return absl::InvalidArgumentError(
234 "Graphics preview format must be snes4bpp");
235 }
236 const int count = transfer.at("byte_count").get<int>();
237 if (count <= 0 || count > 0x8000 || count % 32 != 0) {
238 return absl::InvalidArgumentError(
239 "Invalid graphics preview byte_count");
240 }
241 const auto path = std::filesystem::path(filename).parent_path() /
242 transfer.at("path").get<std::string>();
243 std::error_code ec;
244 const auto size = std::filesystem::file_size(path, ec);
245 if (ec)
246 return absl::NotFoundError("Missing graphics preview asset: " +
247 path.string());
248 if (size != static_cast<size_t>(count)) {
249 return absl::InvalidArgumentError(
250 "Graphics preview asset has wrong byte count: " + path.string());
251 }
252 std::vector<uint8_t> bytes(count);
253 std::ifstream asset(path, std::ios::binary);
254 if (!asset.read(reinterpret_cast<char*>(bytes.data()), count)) {
255 return absl::DataLossError("Cannot read graphics preview asset: " +
256 path.string());
257 }
259 bytes, transfer.at("vram_word_address").get<int>(), base);
260 if (!decoded.ok())
261 return decoded.status();
262 for (const auto& [id, pixels] : *decoded) {
263 if (!tiles.emplace(id, pixels).second) {
264 return absl::InvalidArgumentError(
265 "Overlapping graphics preview transfers");
266 }
267 }
268 }
269 }
270 } catch (const nlohmann::json::exception& e) {
271 return absl::InvalidArgumentError(
272 std::string("Invalid graphics preview: ") + e.what());
273 }
274 return result;
275}
276
277} // namespace yaze::core
278#endif
absl::StatusOr< zelda3::OverworldGraphicsPreview > LoadVersion2(const nlohmann::json &document, const std::string &filename)
absl::StatusOr< int > NonnegativeInt(const nlohmann::json &value, const std::string &field)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< zelda3::OverworldGraphicsPreview > LoadOverworldGraphicsPreview(const std::string &filename)
absl::StatusOr< AreaGraphicsPreview > DecodeGraphicsPreviewTransfer(std::span< const uint8_t > bytes, int destination_word, int bg_base_word)
std::map< int, AreaGraphicsPreview > OverworldGraphicsPreview
Room transition destination.
Definition zelda.h:448