230 const std::
string& portable_catalog,
233 if (source_root.empty() || portable_catalog.empty())
234 return absl::FailedPreconditionError(
235 "Bind an Oracle source repository and project graphics catalog first");
236 auto root = Resolve(source_root);
238 return root.status();
239 auto portable_path = Resolve(portable_catalog);
240 if (!portable_path.ok())
241 return portable_path.status();
242 auto canonical_path = Resolve(*root /
"Overworld/graphics_catalog.json");
243 if (!canonical_path.ok())
244 return canonical_path.status();
245 std::map<std::string, std::string> hashes;
247 auto canonical = LoadCatalog(*canonical_path, &hashes);
249 return canonical.status();
250 auto portable = LoadCatalog(*portable_path, &hashes);
252 return portable.status();
253 if (Semantics(*canonical) != Semantics(*portable))
254 return absl::FailedPreconditionError(
255 "Source and portable graphics catalogs differ; rebuild or rebind the "
257 std::string selected_profile;
258 std::map<std::string, std::vector<int>> parents_by_profile;
259 for (
const auto& binding : canonical->at(
"bindings")) {
260 const auto profile = binding.at(
"profile_id").get<std::string>();
261 for (
int parent : binding.at(
"parent_maps")) {
262 parents_by_profile[profile].push_back(parent);
263 if (parent == parent_map)
264 selected_profile = profile;
267 if (selected_profile.empty())
268 return absl::NotFoundError(
269 "This parent map has no project graphics source profile");
270 std::set<std::string> bound_resources;
271 for (
const auto& transfer :
272 canonical->at(
"profiles").at(selected_profile).at(
"transfers"))
273 bound_resources.insert(transfer.at(
"resource_id").get<std::string>());
274 std::vector<Resource> resources;
275 for (
const auto&
id : bound_resources) {
276 const auto& entry = canonical->at(
"resources").at(
id);
279 auto source_path = Resolve(canonical_path->parent_path() /
280 entry.at(
"path").get<std::string>());
281 auto pack_path = Resolve(
282 portable_path->parent_path() /
283 portable->at(
"resources").at(
id).at(
"path").get<std::string>());
284 if (!source_path.ok())
285 return source_path.status();
287 return pack_path.status();
290 const auto identity = hashes.find(resource.
source_path);
291 if (identity == hashes.end())
292 return absl::FailedPreconditionError(
293 "Graphics resource path changed while loading; rebind the source "
295 resource.
sha256 = identity->second;
296 auto bytes = Read(*source_path);
298 return bytes.status();
300 return absl::FailedPreconditionError(
301 "Graphics source changed while loading; reload the source "
308 return decoded.status();
309 for (
const auto& [tile, pixels] : *decoded)
310 resource.
tiles.push_back(pixels);
311 std::set<int> affected_parents;
312 for (
const auto& [profile_id, profile] :
313 canonical->at(
"profiles").items()) {
315 for (
const auto& transfer : profile.at(
"transfers")) {
316 if (transfer.at(
"resource_id") != id)
321 slice.
parent_maps = parents_by_profile[profile_id];
322 slice.
source_offset = transfer.value(
"source_offset",
size_t{0});
323 slice.
byte_count = transfer.at(
"byte_count").get<
size_t>();
326 profile.at(
"bg_character_base_word").get<
int>();
327 resource.
slices.push_back(std::move(slice));
331 const auto& parents = parents_by_profile[profile_id];
332 affected_parents.insert(parents.begin(), parents.end());
335 resource.
parent_maps.assign(affected_parents.begin(),
336 affected_parents.end());
337 resources.push_back(std::move(resource));
339 source_root_ = root->string();
340 portable_catalog_ = portable_path->string();
341 parent_map_ = parent_map;
342 profile_id_ = selected_profile;
343 resources_ = std::move(resources);
344 histories_.resize(resources_.size());
345 loaded_hashes_ = std::move(hashes);
346 auto status = CheckUnchanged();
352 return absl::InvalidArgumentError(
353 std::string(
"Invalid graphics source catalog: ") + error.
what());
492 size_t index,
const std::vector<uint8_t>& bytes) {
494 return absl::FailedPreconditionError(
495 "Load a project graphics resource first");
497 if (bytes.empty() || bytes.size() % 32 != 0 ||
498 bytes.size() != resource.original_bytes.size())
499 return absl::InvalidArgumentError(
500 "Replacement must preserve the resource's aligned 4bpp byte count");
506 return decoded.status();
507 if (bytes == resource.pending_bytes)
508 return absl::OkStatus();
512 std::vector<uint8_t> replacement(bytes);
513 std::vector<zelda3::PreviewTile8> tiles;
514 tiles.reserve(decoded->size());
515 for (
const auto& [character, pixels] : *decoded)
516 tiles.push_back(pixels);
517 resource.pending_bytes.swap(replacement);
518 resource.tiles.swap(tiles);
522 return absl::OkStatus();
528 return absl::FailedPreconditionError(
529 "Load a project graphics resource first");
535 for (
const auto& slice : resource.slices) {
536 if (slice.source_offset > resource.pending_bytes.size() ||
538 resource.pending_bytes.size() - slice.source_offset)
539 return absl::InvalidArgumentError(
540 "Pending graphics resource slice is outside its resource");
542 std::span<const uint8_t>(resource.pending_bytes)
543 .subspan(slice.source_offset, slice.byte_count),
544 slice.vram_word_address, slice.bg_character_base_word);
546 return decoded.status();
547 for (
int parent : slice.parent_maps) {
548 auto& area = result[parent];
549 for (
const auto& [character, pixels] : *decoded) {
550 if (!area.emplace(character, pixels).second)
551 return absl::InvalidArgumentError(
552 "Overlapping pending graphics preview transfers");
582 return absl::FailedPreconditionError(
583 "The selected graphics resource has no pending changes");
585 return absl::FailedPreconditionError(
586 "The bound source repository has no supported graphics publisher at "
587 "Scripts/Graphics/edit_graphics_source.py");
591 auto files = std::make_unique<PreviewFiles>();
592 std::error_code error;
593 const auto temporary_root = fs::temp_directory_path(error);
595 return absl::InternalError(
596 "Cannot locate a private graphics preview directory");
597 const auto temp = fs::canonical(temporary_root, error);
599 return absl::InternalError(
600 "Cannot resolve the private graphics preview directory");
601 static std::atomic<uint64_t> sequence{0};
602 for (
int attempt = 0; attempt < 100; ++attempt) {
603 const auto candidate =
605 (
"yaze-graphics-source-" +
607 std::chrono::steady_clock::now().time_since_epoch().count()) +
608 "-" + std::to_string(sequence++));
609 if (fs::create_directory(candidate, error)) {
610 files->directory = candidate;
613 if (error && error != std::errc::file_exists)
614 return absl::InternalError(
615 "Cannot create a private graphics preview directory");
617 if (files->directory.empty())
618 return absl::AlreadyExistsError(
619 "Cannot reserve a private graphics preview directory");
620 fs::permissions(files->directory, fs::perms::owner_all, error);
622 return absl::InternalError(
623 "Cannot protect the private graphics preview directory");
624 files->replacement = files->directory /
"replacement.4bpp";
625 files->plan = files->directory /
"review-plan.json";
627 status = Write(files->replacement, resource.pending_bytes);
630 files->replacement_sha256 = Hash(resource.pending_bytes);
631 auto result = RunPublisher(
633 (fs::path(
source_root_) /
"Scripts/Graphics/edit_graphics_source.py")
635 "plan",
"--source-root",
source_root_,
"--portable-catalog",
637 files->replacement.string(),
"--plan", files->plan.string()},
640 return result.status();
641 auto plan_bytes = Read(files->plan);
642 if (!plan_bytes.ok())
643 return plan_bytes.status();
644 auto plan = Parse(*plan_bytes);
646 return plan.status();
648 if (*plan != *result || plan->at(
"operation") !=
"plan" ||
649 plan->at(
"result") !=
"ok" ||
650 plan->at(
"before_resource_id") != resource.id ||
651 plan->at(
"resource_sha256") != resource.sha256 ||
652 plan->at(
"replacement_sha256") != files->replacement_sha256 ||
655 plan->at(
"replacement_path") != files->replacement.string() ||
656 !plan->at(
"after_resource_id").is_string())
657 return absl::DataLossError(
658 "Graphics publisher plan does not match the selected draft");
660 return absl::DataLossError(
661 "Graphics publisher returned an incomplete review plan");