yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_graphics_import.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <fstream>
5#include <map>
6#include <set>
7#include <stdexcept>
8#include <string_view>
9#include <tuple>
10#include <utility>
11
12#include "absl/status/status.h"
13#include "absl/strings/str_format.h"
16#include "nlohmann/json.hpp"
17#include "zelda3/game_data.h"
21
22namespace yaze::editor {
23namespace {
24using Json = nlohmann::json;
26using ColorKey = std::tuple<std::string, int, int>;
27
28absl::Status Invalid(std::string message) {
29 return absl::InvalidArgumentError(std::move(message));
30}
31
32int Integer(const Json& value, int maximum) {
33 if (!value.is_number_integer() ||
34 (value.is_number_unsigned()
35 ? value.get<uint64_t>() > maximum
36 : value.get<int64_t>() < 0 || value.get<int64_t>() > maximum)) {
37 throw std::invalid_argument("Invalid import integer");
38 }
39 return value.get<int>();
40}
41
42absl::StatusOr<std::string> ReadFile(const std::filesystem::path& path,
43 size_t maximum = 8 * 1024 * 1024) {
44 std::error_code error;
45 const auto size = std::filesystem::file_size(path, error);
46 if (error)
47 return absl::NotFoundError("Cannot read import: " + path.string());
48 if (size > maximum)
49 return Invalid("Import file exceeds size limit");
50 std::ifstream stream(path, std::ios::binary);
51 std::string bytes(static_cast<size_t>(size), '\0');
52 if (!stream.read(bytes.data(), bytes.size())) {
53 return absl::DataLossError("Import file changed while reading");
54 }
55 return bytes;
56}
57
58Json ParseJson(const std::string& bytes) {
59 std::vector<std::set<std::string>> keys;
60 bool duplicate = false;
61 auto result =
62 Json::parse(bytes, [&](int, Json::parse_event_t event, Json& parsed) {
63 if (event == Json::parse_event_t::object_start)
64 keys.emplace_back();
65 if (event == Json::parse_event_t::key &&
66 !keys.back().insert(parsed.get<std::string>()).second)
67 duplicate = true;
68 if (event == Json::parse_event_t::object_end)
69 keys.pop_back();
70 return true;
71 });
72 if (duplicate)
73 throw std::invalid_argument("Duplicate import JSON key");
74 return result;
75}
76
77absl::StatusOr<std::filesystem::path> RelativePath(
78 const std::filesystem::path& root, const std::string& relative) {
79 const std::filesystem::path value(relative);
80 if (relative.empty() || value.is_absolute() ||
81 relative.find('\\') != std::string::npos) {
82 return Invalid("Import assets must use portable relative paths");
83 }
84 for (const auto& component : value) {
85 if (component == "..")
86 return Invalid("Import path escapes its catalog");
87 }
88 std::error_code error;
89 const auto base = std::filesystem::weakly_canonical(root, error);
90 if (error)
91 return Invalid("Cannot resolve import directory");
92 const auto target = std::filesystem::weakly_canonical(root / value, error);
93 if (error)
94 return Invalid("Cannot resolve import resource");
95 auto b = base.begin();
96 auto t = target.begin();
97 for (; b != base.end(); ++b, ++t) {
98 if (t == target.end() || *b != *t) {
99 return Invalid("Import resource symlink escapes its catalog");
100 }
101 }
102 return target;
103}
104
105Pixels Decode(const std::vector<uint8_t>& bytes, int character) {
106 const auto tile = gfx::UnpackBppTile(bytes, character * 32, 4);
107 Pixels pixels{};
108 std::copy(std::begin(tile.data), std::end(tile.data), pixels.begin());
109 return pixels;
110}
111
112std::vector<uint8_t> Encode(const Pixels& pixels) {
113 snes_tile8 tile{};
114 std::copy(pixels.begin(), pixels.end(), std::begin(tile.data));
115 return gfx::PackBppTile(tile, 4);
116}
117
118Pixels Flip(const Pixels& pixels, bool horizontal, bool vertical) {
119 Pixels result{};
120 for (int y = 0; y < 8; ++y) {
121 for (int x = 0; x < 8; ++x) {
122 result[y * 8 + x] =
123 pixels[(vertical ? 7 - y : y) * 8 + (horizontal ? 7 - x : x)];
124 }
125 }
126 return result;
127}
128
129absl::Status ValidateAsset(const ProjectGraphicsImportAsset& asset) {
130 if (asset.width < 1 || asset.width > 256 || asset.height < 1 ||
131 asset.height > 256 ||
132 asset.cells.size() != static_cast<size_t>(asset.width) * asset.height ||
133 asset.palettes.empty())
134 return Invalid("Invalid logical import dimensions");
135 std::map<std::string, const ProjectImportPalette*> groups;
136 for (const auto& group : asset.palettes) {
137 if (group.id.empty() || !groups.emplace(group.id, &group).second ||
138 group.characters.empty() || group.characters.size() % 32 != 0 ||
139 group.characters.size() > 0x8000 || group.colors[0] ||
140 group.colors[8]) {
141 return Invalid("Invalid or duplicate logical import palette");
142 }
143 for (const auto& color : group.colors) {
144 if (color && *color > 0x7FFF)
145 return Invalid("Import color exceeds BGR555");
146 }
147 for (int c = 0; c < static_cast<int>(group.characters.size() / 32); ++c) {
148 for (const auto index : Decode(group.characters, c)) {
149 if (index != 0 && index != 8 && !group.colors[index]) {
150 return Invalid("Import glyph uses an undefined palette color");
151 }
152 }
153 }
154 }
155 for (const auto& cell : asset.cells) {
156 for (const auto& q : cell.quadrants) {
157 if (q.transparent)
158 continue;
159 const auto found = groups.find(q.palette);
160 if (found == groups.end() || q.character < 0 ||
161 q.character >=
162 static_cast<int>(found->second->characters.size() / 32)) {
163 return Invalid("Unknown logical palette or character in import");
164 }
165 }
166 }
167 return absl::OkStatus();
168}
169
170std::array<uint16_t, 4> Words(const gfx::Tile16& tile) {
171 std::array<uint16_t, 4> words{};
172 for (int q = 0; q < 4; ++q) {
174 }
175 return words;
176}
177
178absl::StatusOr<std::vector<uint16_t>> RenderColors(
179 const gfx::Tile16& tile, const std::vector<Pixels>& characters,
180 const std::array<uint16_t, 128>& palette) {
181 for (int q = 0; q < 4; ++q) {
182 const auto& part = zelda3::Tile16QuadrantInfo(tile, q);
183 if (part.id_ >= characters.size() || part.palette_ > 7) {
184 return Invalid("Current Tile16 references unavailable graphics");
185 }
186 }
187 const auto pixels = zelda3::RenderTile16PixelsFromMetadata(tile, characters);
188 std::vector<uint16_t> colors;
189 colors.reserve(pixels.size());
190 for (auto index : pixels)
191 colors.push_back(palette[index]);
192 return colors;
193}
194} // namespace
195
196absl::StatusOr<ProjectGraphicsImportAsset> LoadProjectGraphicsImport(
197 const std::filesystem::path& logical_pack) {
198 auto bytes = ReadFile(logical_pack);
199 if (!bytes.ok())
200 return bytes.status();
201 try {
202 const auto document = ParseJson(*bytes);
203 auto catalog_path = RelativePath(logical_pack.parent_path(),
204 document.at("catalog").get<std::string>());
205 if (!catalog_path.ok())
206 return catalog_path.status();
207 auto catalog_bytes = ReadFile(*catalog_path);
208 if (!catalog_bytes.ok())
209 return catalog_bytes.status();
210 const auto catalog = ParseJson(*catalog_bytes);
211 if (Integer(catalog.at("schema_version"), 2) != 2 ||
212 !catalog.at("resources").is_object())
213 return Invalid("Expected V2 import catalog");
214 std::map<std::string, std::vector<uint8_t>> resources;
215 for (const auto& [id, resource] : catalog.at("resources").items()) {
216 if (id.empty() || resource.at("format") != "snes4bpp") {
217 return Invalid("Import resource must be named native 4bpp");
218 }
219 const int count = Integer(resource.at("byte_count"), 0x8000);
220 if (!count || count % 32)
221 return Invalid("Unaligned import resource size");
222 auto path = RelativePath(catalog_path->parent_path(),
223 resource.at("path").get<std::string>());
224 if (!path.ok())
225 return path.status();
226 auto body = ReadFile(*path, 0x8000);
227 if (!body.ok())
228 return body.status();
229 if (body->size() != static_cast<size_t>(count) ||
231 resource.at("sha256").get<std::string>()) {
232 return absl::DataLossError("Import resource size or SHA256 mismatch");
233 }
234 resources.emplace(id, std::vector<uint8_t>(body->begin(), body->end()));
235 }
236 auto validated_catalog =
237 core::LoadOverworldGraphicsPreview(catalog_path->string());
238 if (!validated_catalog.ok())
239 return validated_catalog.status();
242 const auto& dimensions = document.at("tile16_grid_size");
243 if (!dimensions.is_array() || dimensions.size() != 2)
244 return Invalid("Invalid import grid");
245 asset.width = Integer(dimensions[0], 256);
246 asset.height = Integer(dimensions[1], 256);
247 if (!asset.width || !asset.height)
248 return Invalid("Empty import grid");
249 if (document.contains("source_dimensions")) {
250 const auto& pixels = document.at("source_dimensions");
251 if (!pixels.is_array() || pixels.size() != 2 ||
252 Integer(pixels[0], 4096) != asset.width * 16 ||
253 Integer(pixels[1], 4096) != asset.height * 16) {
254 return Invalid(
255 "Source pixel dimensions differ from the logical Tile16 grid");
256 }
257 }
258 asset.cells.resize(asset.width * asset.height);
259 std::set<std::string> resource_groups;
260 if (!document.at("resources").is_array() ||
261 !document.at("palette_groups").is_array() ||
262 !document.at("tile16_cells").is_array())
263 return Invalid("Logical import tables must be arrays");
264 for (const auto& item : document.at("resources")) {
265 if (item.contains("physical_destinations") &&
266 !item.at("physical_destinations").is_null())
267 return Invalid(
268 "Logical source cannot grant physical character destinations");
269 const auto id = item.at("logical_palette").get<std::string>();
270 const auto source =
271 resources.find(item.at("resource_id").get<std::string>());
272 if (!resource_groups.insert(id).second || source == resources.end()) {
273 return Invalid("Duplicate or unknown import palette resource");
274 }
276 group.id = id;
277 group.characters = source->second;
278 if (item.contains("bytes") &&
279 Integer(item.at("bytes"), 0x8000) != group.characters.size())
280 return Invalid("Logical import byte count differs from resource");
281 if (item.contains("characters") && Integer(item.at("characters"), 1024) !=
282 group.characters.size() / 32) {
283 return Invalid("Logical import character count differs from resource");
284 }
285 const Json* palette = nullptr;
286 for (const auto& entry : document.at("palette_groups")) {
287 if (entry.at("logical_palette") == id) {
288 if (palette)
289 return Invalid("Duplicate logical import palette");
290 palette = &entry;
291 }
292 }
293 if (!palette)
294 return Invalid("Missing logical import palette colors");
295 if (palette->contains("physical_BG_row") &&
296 !palette->at("physical_BG_row").is_null())
297 return Invalid(
298 "Logical source cannot grant a physical palette destination");
299 const auto& words = palette->at("BGR555_words");
300 if (!words.is_array() || words.size() != 16)
301 return Invalid("Import palette needs16 entries");
302 const Json* rgb = palette->contains("RGB_entries")
303 ? &palette->at("RGB_entries")
304 : nullptr;
305 if (rgb && (!rgb->is_array() || rgb->size() != 16))
306 return Invalid("Invalid import RGB entries");
307 for (int i = 0; i < 16; ++i) {
308 if (words[i].is_null()) {
309 if (rgb && !(*rgb)[i].is_null())
310 return Invalid("RGB entry has no BGR555 source");
311 continue;
312 }
313 const int word = Integer(words[i], 0x7FFF);
314 if (rgb && (*rgb)[i].is_null())
315 continue;
316 if (i == 0 || i == 8) {
317 if (rgb || word != 0)
318 return Invalid("Import indices0/8 must remain transparent");
319 continue;
320 }
321 if (rgb) {
322 if (!(*rgb)[i].is_array() || (*rgb)[i].size() != 3)
323 return Invalid("Invalid import RGB color");
324 for (int c = 0; c < 3; ++c) {
325 const int component = (word >> (5 * c)) & 31;
326 if (Integer((*rgb)[i][c], 255) !=
327 ((component << 3) | (component >> 2))) {
328 return Invalid("Import RGB color does not exactly match BGR555");
329 }
330 }
331 }
332 group.colors[i] = static_cast<uint16_t>(word);
333 }
334 asset.palettes.push_back(std::move(group));
335 }
336 if (document.at("palette_groups").size() != asset.palettes.size())
337 return Invalid("Unmatched logical palettes");
338 std::set<std::pair<int, int>> positions;
339 for (const auto& item : document.at("tile16_cells")) {
340 if (item.contains("physical_Tile16_id") &&
341 !item.at("physical_Tile16_id").is_null())
342 return Invalid(
343 "Logical source cannot grant a physical Tile16 destination");
344 const int x = Integer(item.at("x"), asset.width - 1);
345 const int y = Integer(item.at("y"), asset.height - 1);
346 if (!positions.emplace(x, y).second || item.at("quadrants").size() != 4) {
347 return Invalid("Duplicate import cell or invalid quadrants");
348 }
349 for (int i = 0; i < 4; ++i) {
350 const auto& part = item.at("quadrants")[i];
351 auto& q = asset.cells[y * asset.width + x].quadrants[i];
352 if (part.value("transparent", false)) {
353 if (part.contains("logical_palette") ||
354 part.contains("logical_character"))
355 return Invalid(
356 "Transparent import quadrant has conflicting art references");
357 continue;
358 }
359 q.transparent = false;
360 q.palette = part.at("logical_palette").get<std::string>();
361 q.character = Integer(part.at("logical_character"), 1023);
362 q.hflip = part.at("hflip").get<bool>();
363 q.vflip = part.at("vflip").get<bool>();
364 }
365 }
366 if (positions.size() != asset.cells.size())
367 return Invalid("Import grid has missing cells");
368 const auto status = ValidateAsset(asset);
369 if (!status.ok())
370 return status;
371 return asset;
372 } catch (const std::exception& error) {
373 return Invalid(std::string("Invalid graphics import: ") + error.what());
374 }
375}
376
377absl::StatusOr<ProjectGraphicsImportContext>
379 const std::vector<gfx::Tile16>& definitions,
380 const std::vector<uint16_t>& grid,
381 int width, int height) {
382 if (!map.game_data() || map.current_graphics().size() < 65536 ||
383 map.current_palette().size() < 128 || width < 1 || height < 1 ||
384 width > 256 || height > 256 ||
385 grid.size() != static_cast<size_t>(width) * height) {
386 return absl::FailedPreconditionError(
387 "Load the current map's resolved graphics and palette first");
388 }
390 result.owner = map.game_data();
392 result.definitions = definitions;
393 result.grid = grid;
394 result.grid_width = width;
395 result.grid_height = height;
396 result.characters.resize(1024);
397 for (int c = 0; c < 1024; ++c) {
398 for (int y = 0; y < 8; ++y) {
399 for (int x = 0; x < 8; ++x) {
400 result.characters[c][y * 8 + x] =
401 map.current_graphics()[(c / 16 * 8 + y) * 128 + c % 16 * 8 + x] &
402 15;
403 }
404 }
405 }
406 for (int i = 0; i < 128; ++i)
407 result.palette[i] = map.current_palette()[i].snes();
408 return result;
409}
410
411absl::StatusOr<ProjectGraphicsImportPlan> PlanProjectGraphicsImport(
412 const ProjectGraphicsImportAsset& asset,
413 const ProjectGraphicsImportBindings& bindings,
414 const ProjectGraphicsImportContext& context) {
415 auto status = ValidateAsset(asset);
416 if (!status.ok())
417 return status;
418 if (!context.owner || context.grid_width < 1 || context.grid_width > 256 ||
419 context.grid_height < 1 || context.grid_height > 256 ||
420 context.grid.size() !=
421 static_cast<size_t>(context.grid_width) * context.grid_height ||
422 context.characters.empty() || context.characters.size() > 1024 ||
423 context.definitions.empty() || context.definitions.size() > 65536 ||
424 bindings.anchor_x < 0 || bindings.anchor_y < 0 ||
425 bindings.anchor_x > context.grid_width - asset.width ||
426 bindings.anchor_y > context.grid_height - asset.height)
427 return Invalid("Invalid import context or anchor");
428 const int resource_count = bindings.target_resource.size() / 32;
429 if (bindings.target_resource.empty() ||
430 bindings.target_resource.size() % 32 ||
431 bindings.target_resource.size() > 0x8000 ||
432 bindings.resource_first_character < 0 ||
433 bindings.resource_first_character > 1023 ||
434 bindings.resource_first_character + resource_count >
435 context.characters.size() ||
436 bindings.first_tile16 < 0 || bindings.owned_tile16_count < 1 ||
437 bindings.first_tile16 > static_cast<int>(context.definitions.size()) -
438 bindings.owned_tile16_count) {
439 return Invalid("Invalid caller-owned graphics or Tile16 range");
440 }
441 for (int i = 0; i < resource_count; ++i) {
442 if (Decode(bindings.target_resource, i) !=
443 context.characters[bindings.resource_first_character + i]) {
444 return absl::FailedPreconditionError(
445 "Pending resource differs from the resolved current map; refresh "
446 "before import");
447 }
448 }
449 struct Group {
452 int next = 0;
453 std::map<std::vector<uint8_t>, int> encoded;
454 };
455 std::map<std::string, Group> groups;
456 std::set<int> rows, allocated;
458 plan.source_sha256 = asset.source_sha256;
459 plan.resource_before = bindings.target_resource;
460 plan.resource_after = bindings.target_resource;
461 auto characters = context.characters;
462 auto definitions = context.definitions;
463 for (const auto& source : asset.palettes) {
464 const ProjectImportPaletteBinding* selected = nullptr;
465 for (const auto& binding : bindings.palettes) {
466 if (binding.palette == source.id) {
467 if (selected)
468 return Invalid("Duplicate logical palette destination");
469 selected = &binding;
470 }
471 }
472 if (!selected || selected->bg_row < 2 || selected->bg_row > 7 ||
473 !rows.insert(selected->bg_row).second ||
474 selected->owned_character_count < 1 ||
475 selected->first_character < bindings.resource_first_character ||
476 selected->first_character > bindings.resource_first_character +
477 resource_count -
478 selected->owned_character_count ||
479 source.characters.size() / 32 > selected->owned_character_count) {
480 return Invalid(
481 "Missing, overlapping or out-of-range owned palette/CHR destination");
482 }
483 Group group{
484 source, *selected, static_cast<int>(source.characters.size() / 32), {}};
485 for (int i = 0; i < selected->owned_character_count; ++i) {
486 if (!allocated.insert(selected->first_character + i).second)
487 return Invalid("Owned character ranges overlap");
488 }
489 for (int i = 0; i < group.next; ++i) {
490 const int c = selected->first_character + i;
491 const auto begin = source.characters.begin() + i * 32;
492 std::vector<uint8_t> encoded(begin, begin + 32);
493 std::copy(encoded.begin(), encoded.end(),
494 plan.resource_after.begin() +
495 (c - bindings.resource_first_character) * 32);
496 characters[c] = Decode(source.characters, i);
497 group.encoded.emplace(std::move(encoded), c);
498 }
499 groups.emplace(source.id, std::move(group));
500 }
501 if (groups.size() != bindings.palettes.size())
502 return Invalid("Unknown palette destination");
503 std::map<int, std::vector<uint16_t>> backgrounds;
504 for (auto id : context.grid) {
505 if (id >= context.definitions.size())
506 return Invalid("Current grid references an unknown Tile16");
507 if (!backgrounds.contains(id)) {
508 auto colors = RenderColors(context.definitions[id], context.characters,
509 context.palette);
510 if (!colors.ok())
511 return colors.status();
512 backgrounds.emplace(id, std::move(*colors));
513 }
514 }
515 plan.stamp.width = asset.width;
516 plan.stamp.height = asset.height;
517 std::map<std::array<uint16_t, 4>, int> imported;
518 std::vector<std::vector<uint16_t>> expected(asset.cells.size());
519 int next_tile = bindings.first_tile16;
520 for (int y = 0; y < asset.height; ++y) {
521 for (int x = 0; x < asset.width; ++x) {
522 const int at = y * asset.width + x;
523 const auto old_id =
524 context.grid[(bindings.anchor_y + y) * context.grid_width +
525 bindings.anchor_x + x];
526 auto tile = context.definitions[old_id];
527 expected[at] = backgrounds.at(old_id);
528 bool opaque = false;
529 for (int q = 0; q < 4; ++q) {
530 const auto& source = asset.cells[at].quadrants[q];
531 if (source.transparent)
532 continue;
533 auto& group = groups.at(source.palette);
534 auto pixels = Flip(Decode(group.source.characters, source.character),
535 source.hflip, source.vflip);
536 const bool any = std::any_of(pixels.begin(), pixels.end(),
537 [](int p) { return p != 0 && p != 8; });
538 if (!any)
539 continue;
540 opaque = true;
541 const bool alpha = std::any_of(pixels.begin(), pixels.end(),
542 [](int p) { return p == 0 || p == 8; });
543 for (int i = 0; i < 64; ++i) {
544 const int location = (q / 2 * 8 + i / 8) * 16 + q % 2 * 8 + i % 8;
545 if (pixels[i] == 0 || pixels[i] == 8) {
546 const auto color = expected[at][location];
547 int found = -1, free = -1;
548 for (int p = 1; p < 16; ++p) {
549 if (p == 8)
550 continue;
551 if (group.source.colors[p] == color)
552 found = p;
553 if (!group.source.colors[p] && free == -1)
554 free = p;
555 }
556 if (found == -1) {
557 if (free == -1)
558 return absl::FailedPreconditionError(
559 "Partial-alpha background color does not fit the logical "
560 "palette; no recoloring applied");
561 found = free;
562 group.source.colors[found] = color;
563 }
564 pixels[i] = found;
565 } else {
566 expected[at][location] = *group.source.colors[pixels[i]];
567 }
568 }
569 int c = group.binding.first_character + source.character;
570 bool h = source.hflip, v = source.vflip;
571 if (alpha) {
572 Pixels canonical = pixels;
573 h = false;
574 v = false;
575 for (auto flags :
576 {std::pair{true, false}, {false, true}, {true, true}}) {
577 auto candidate = Flip(pixels, flags.first, flags.second);
578 if (candidate < canonical) {
579 canonical = candidate;
580 h = flags.first;
581 v = flags.second;
582 }
583 }
584 auto encoded = Encode(canonical);
585 const auto existing = group.encoded.find(encoded);
586 if (existing != group.encoded.end())
587 c = existing->second;
588 else {
589 if (group.next >= group.binding.owned_character_count)
590 return absl::FailedPreconditionError(
591 "Exact alpha composition exceeds the caller-owned character "
592 "range");
593 c = group.binding.first_character + group.next++;
594 group.encoded.emplace(encoded, c);
595 characters[c] = canonical;
596 std::copy(encoded.begin(), encoded.end(),
597 plan.resource_after.begin() +
598 (c - bindings.resource_first_character) * 32);
599 }
600 }
602 gfx::TileInfo(c, group.binding.bg_row, v, h, false);
603 }
604 if (!opaque) {
605 plan.stamp.tile_ids.push_back(old_id);
606 continue;
607 }
608 const auto words = Words(tile);
609 auto found = imported.find(words);
610 int id;
611 if (found != imported.end())
612 id = found->second;
613 else {
614 if (next_tile >= bindings.first_tile16 + bindings.owned_tile16_count)
615 return absl::FailedPreconditionError(
616 "Import exceeds the caller-owned Tile16 range");
617 id = next_tile++;
618 imported.emplace(words, id);
619 if (definitions[id] != tile)
620 plan.definitions.push_back({id, tile});
621 definitions[id] = tile;
622 }
623 plan.stamp.tile_ids.push_back(id);
624 }
625 }
626 auto palette = context.palette;
627 std::map<ColorKey, gfx::PaletteColorChange> changes;
628 for (const auto& [id, group] : groups) {
629 for (int p = 1; p < 16; ++p) {
630 if (p == 8 || !group.source.colors[p])
631 continue;
632 const int index = group.binding.bg_row * 16 + p;
633 const auto& address = context.palette_addresses[index];
634 if (!address)
635 return absl::FailedPreconditionError(
636 "Selected palette color has no writable resolved source");
637 auto original = zelda3::ReadPaletteColor(*context.owner, *address);
638 if (!original.ok())
639 return original.status();
640 if (original->snes() != context.palette[index])
641 return absl::FailedPreconditionError(
642 "Resolved palette changed since planning; refresh the current map");
643 const ColorKey key{zelda3::PaletteAddressGroupName(address->group),
644 address->palette_index, address->color_index};
645 gfx::PaletteColorChange change{std::get<0>(key),
646 std::get<1>(key),
647 std::get<2>(key),
648 *original,
649 gfx::SnesColor(*group.source.colors[p]),
650 0};
651 const auto [at, inserted] = changes.emplace(key, change);
652 if (!inserted && at->second.new_color.snes() != change.new_color.snes())
653 return Invalid(
654 "Logical palettes require conflicting writes to a shared saved "
655 "color");
656 }
657 }
658 for (int i = 0; i < 128; ++i) {
659 if (!context.palette_addresses[i])
660 continue;
661 const auto& a = *context.palette_addresses[i];
662 const auto change = changes.find({zelda3::PaletteAddressGroupName(a.group),
663 a.palette_index, a.color_index});
664 if (change != changes.end())
665 palette[i] = change->second.new_color.snes();
666 }
667 for (const auto& [key, change] : changes) {
668 if (change.original_color.snes() != change.new_color.snes())
669 plan.palette_changes.push_back(change);
670 }
671 // Render every placed consumer after the proposed changes. This catches
672 // aliases between palette halves and glyph/definition consumers outside the
673 // requested stamp. Global ownership still belongs to the caller.
674 for (int y = 0; y < context.grid_height; ++y) {
675 for (int x = 0; x < context.grid_width; ++x) {
676 const int old = context.grid[y * context.grid_width + x];
677 const int sx = x - bindings.anchor_x, sy = y - bindings.anchor_y;
678 const bool inside =
679 sx >= 0 && sy >= 0 && sx < asset.width && sy < asset.height;
680 const int id = inside ? plan.stamp.at(sx, sy) : old;
681 auto rendered = RenderColors(definitions[id], characters, palette);
682 if (!rendered.ok())
683 return rendered.status();
684 const auto& wanted =
685 inside ? expected[sy * asset.width + sx] : backgrounds.at(old);
686 if (*rendered != wanted) {
687 const auto mismatch =
688 std::mismatch(rendered->begin(), rendered->end(), wanted.begin());
689 const int pixel = static_cast<int>(mismatch.first - rendered->begin());
690 return absl::FailedPreconditionError(absl::StrFormat(
691 "Import pixel mismatch %s: area tile=(%d,%d), Tile16=0x%03X "
692 "(previous=0x%03X), pixel=(%d,%d), wanted BGR555=0x%04X, "
693 "rendered BGR555=0x%04X",
694 inside ? "inside import stamp" : "outside existing consumer", x, y,
695 id, old, pixel % 16, pixel / 16, *mismatch.second,
696 *mismatch.first));
697 }
698 }
699 }
700 for (int i = 0; i < resource_count; ++i) {
701 const int c = bindings.resource_first_character + i;
702 if (std::equal(plan.resource_before.begin() + i * 32,
703 plan.resource_before.begin() + (i + 1) * 32,
704 plan.resource_after.begin() + i * 32))
705 continue;
707 report.character = c;
708 for (const auto& tile : context.definitions) {
709 for (int q = 0; q < 4; ++q)
710 if (zelda3::Tile16QuadrantInfo(tile, q).id_ == c)
711 ++report.definition_consumers;
712 }
713 for (auto id : context.grid) {
714 for (int q = 0; q < 4; ++q)
715 if (zelda3::Tile16QuadrantInfo(context.definitions[id], q).id_ == c)
716 ++report.placed_consumers;
717 }
718 plan.replacements.push_back(report);
719 }
720 plan.notes.push_back(
721 "Only caller-owned ranges are planned; current-map pixel equality does "
722 "not prove global allocation or collision ownership.");
723 return plan;
724}
725} // namespace yaze::editor
static Json parse(const std::string &)
Definition json.h:36
bool is_array() const
Definition json.h:58
bool is_null() const
Definition json.h:54
size_t size() const
Definition json.h:61
bool contains(const std::string &) const
Definition json.h:53
SNES Color container.
Definition snes_color.h:110
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
Represents a single Overworld map screen.
const gfx::SnesPalette & current_palette() const
const std::vector< uint8_t > & current_graphics() const
const GameData * game_data() const
const PaletteAddressMap & palette_addresses() const
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< zelda3::OverworldGraphicsPreview > LoadOverworldGraphicsPreview(const std::string &filename)
absl::StatusOr< std::filesystem::path > RelativePath(const std::filesystem::path &root, const std::string &relative)
Pixels Flip(const Pixels &pixels, bool horizontal, bool vertical)
std::array< uint16_t, 4 > Words(const gfx::Tile16 &tile)
absl::StatusOr< std::vector< uint16_t > > RenderColors(const gfx::Tile16 &tile, const std::vector< Pixels > &characters, const std::array< uint16_t, 128 > &palette)
absl::Status ValidateAsset(const ProjectGraphicsImportAsset &asset)
Pixels Decode(const std::vector< uint8_t > &bytes, int character)
Editors are the view controllers for the application.
absl::StatusOr< ProjectGraphicsImportAsset > LoadProjectGraphicsImport(const std::filesystem::path &logical_pack)
absl::StatusOr< ProjectGraphicsImportContext > CaptureProjectGraphicsImportContext(const zelda3::OverworldMap &map, const std::vector< gfx::Tile16 > &definitions, const std::vector< uint16_t > &grid, int width, int height)
absl::StatusOr< ProjectGraphicsImportPlan > PlanProjectGraphicsImport(const ProjectGraphicsImportAsset &asset, const ProjectGraphicsImportBindings &bindings, const ProjectGraphicsImportContext &context)
snes_tile8 UnpackBppTile(std::span< const uint8_t > data, const uint32_t offset, const uint32_t bpp)
Definition snes_tile.cc:25
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
std::vector< uint8_t > PackBppTile(const snes_tile8 &tile, const uint32_t bpp)
Definition snes_tile.cc:74
gfx::TileInfo & MutableTile16QuadrantInfo(gfx::Tile16 &tile, int quadrant)
std::vector< uint8_t > RenderTile16PixelsFromMetadata(const gfx::Tile16 &tile_data, const std::vector< Tile8PixelData > &tile8_pixels)
const char * PaletteAddressGroupName(PaletteAddressGroup group)
const gfx::TileInfo & Tile16QuadrantInfo(const gfx::Tile16 &tile, int quadrant)
std::array< uint8_t, 64 > Tile8PixelData
absl::StatusOr< gfx::SnesColor > ReadPaletteColor(const GameData &owner, const PaletteColorAddress &address)
8x8 SNES tile data
std::vector< ProjectImportPalette > palettes
std::vector< ProjectImportTile16 > cells
std::vector< ProjectImportPaletteBinding > palettes
std::vector< zelda3::Tile8PixelData > characters
std::vector< ProjectImportReplacement > replacements
std::vector< gfx::PaletteColorChange > palette_changes
std::array< std::optional< uint16_t >, 16 > colors
int at(int x, int y) const
Definition tile_brush.h:24
std::vector< int > tile_ids
Definition tile_brush.h:15
Represents a single color change operation.