yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
usage_preview_view.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <set>
5
6#include "absl/strings/str_format.h"
10#include "app/gui/core/icons.h"
12#include "imgui/imgui.h"
13#include "util/i18n/tr.h"
14#include "util/macro.h"
19
20namespace yaze::editor {
21
22namespace {
23
24constexpr uint64_t kFnvOffset = 1469598103934665603ULL;
25constexpr uint64_t kFnvPrime = 1099511628211ULL;
26
27uint64_t HashBytes(uint64_t hash, const std::vector<uint8_t>& bytes) {
28 for (const uint8_t b : bytes) {
29 hash = (hash ^ b) * kFnvPrime;
30 }
31 return hash;
32}
33
40
41} // namespace
42
44 rom_ = rom;
45 // Explicit rebinding also covers replacement at the same Rom address.
47}
48
53
55 shared_scan_ready_ = false;
56 shared_scan_status_.clear();
57 shared_targets_.clear();
58 shared_uses_.clear();
61 source_dirty_ = true;
66 tables_ready_ = false;
67 room_blocks_.clear();
69 live_rooms_.clear();
71 rooms_for_sheet_.clear();
72 object_rects_.clear();
74 group_hash_ = 0;
75 last_render_time_ = -1.0;
76 room_gfx_.clear();
77 areas_.reset();
79 tile16s_.clear();
82 last_sheet_ = 0xFFFF;
83 dirty_ = true;
84}
85
87 if (!rom_ || !rom_->is_loaded() || !game_data_)
88 return;
89 uint64_t hash = kFnvOffset;
90 auto groups = [&](const auto& table) {
91 for (const auto& row : table)
92 for (const auto id : row)
93 hash = (hash ^ id) * kFnvPrime;
94 };
98 if (tables_ready_ && group_hash_ == hash)
99 return;
100 group_hash_ = hash;
105 last_sheet_ = 0xFFFF;
106 dirty_ = true;
107 tables_ready_ = true;
108}
109
113 auto blocks = fallback_room_blocks_;
114 std::map<int, uint64_t> revisions;
115 int current = -1;
116 for (auto it = contexts.begin(); it != contexts.end();) {
117 auto* room = it->second.room;
118 if (!room || room->rom() != rom_ || room->game_data() != game_data_ ||
119 !room->IsLoaded() || room->id() != it->first) {
120 it = contexts.erase(it);
121 continue;
122 }
123 std::copy_n(room->blocks().begin(), 16, blocks[it->first].begin());
124 revisions[it->first] = room->composite_source_revision();
125 if (it->second.current)
126 current = it->first;
127 ++it;
128 }
129 const bool changed = blocks != room_blocks_ ||
130 revisions != live_room_revisions_ ||
131 current != current_live_room_;
132 if (changed || last_sheet_ != state_->current_sheet_id) {
133 const int previous =
134 room_choice_ >= 0 &&
135 room_choice_ < static_cast<int>(rooms_for_sheet_.size())
137 : -1;
140 // Keep the selected live room inspectable even if it stops loading the
141 // edited sheet; that absence is useful feedback, not a reason to hide it.
142 if (current >= 0 &&
143 std::find(rooms_for_sheet_.begin(), rooms_for_sheet_.end(), current) ==
144 rooms_for_sheet_.end())
145 rooms_for_sheet_.push_back(current);
146 const int preferred =
148 ? current
149 : previous;
150 const auto selected =
151 std::find(rooms_for_sheet_.begin(), rooms_for_sheet_.end(), preferred);
152 room_choice_ = selected == rooms_for_sheet_.end()
153 ? 0
154 : static_cast<int>(selected - rooms_for_sheet_.begin());
155 dirty_ = true;
156 last_render_time_ = -1.0;
157 }
158 room_blocks_ = std::move(blocks);
159 live_room_revisions_ = std::move(revisions);
160 live_rooms_ = std::move(contexts);
161 current_live_room_ = current;
162}
163
164uint64_t UsagePreviewView::HashWatchedSheets(bool include_room_context) const {
165 uint64_t hash = kFnvOffset;
166 // Other sheet editors can commit a sheet this room uses without adding it
167 // to GraphicsEditor's modified_sheets. Watch only the chosen room's store
168 // revisions here; membership refresh must not rasterize loaded rooms.
169 if (include_room_context && room_choice_ >= 0 &&
170 room_choice_ < static_cast<int>(rooms_for_sheet_.size())) {
171 const auto found = room_blocks_.find(rooms_for_sheet_[room_choice_]);
172 if (found != room_blocks_.end()) {
173 for (const auto sheet : found->second) {
174 hash = (hash ^ sheet) * kFnvPrime;
175 hash = (hash ^ game_data_->sheet_store.Revision(sheet)) * kFnvPrime;
176 }
177 }
178 }
179 std::set<uint16_t> ids = state_->modified_sheets;
180 ids.insert(state_->current_sheet_id);
181 if (state_->source_context) {
182 for (const auto& target : state_->source_context->targets) {
183 ids.insert(target.sheet_id);
184 }
185 for (const auto& slot : state_->source_context->resolved.slots) {
186 ids.insert(slot.sheet_id);
187 }
188 }
189 const auto sheets = usage_preview::SnapshotSessionSheets(ids, game_data_);
190 for (const auto& [id, pixels] : sheets) {
191 hash = (hash ^ id) * kFnvPrime;
192 hash = HashBytes(hash, pixels);
193 }
194 return hash;
195}
196
202
204 const auto& selection = state_->selection;
205 if (follow_selection_ && selection.is_active) {
206 const int tile = (selection.y / 8) * 16 + selection.x / 8;
207 return std::clamp(tile, 0, 63);
208 }
209 return tile_index_;
210}
211
213 const usage_preview::PreviewImage& image) {
214 if (image.empty()) {
215 return;
216 }
217 const bool indexed =
219 const int depth = indexed ? 8 : 32;
220 std::vector<SDL_Color> colors(image.colors.begin(), image.colors.end());
221 if (image.index0_transparent) {
222 colors[0].a = 0;
223 }
224 if (bitmap.width() != image.width || bitmap.height() != image.height ||
225 !bitmap.is_active() || bitmap.depth() != depth) {
226 bitmap.Create(image.width, image.height, depth, image.pixels);
227 } else {
228 bitmap.set_data(image.pixels);
229 }
230 if (indexed)
231 bitmap.SetPalette(colors);
233 QueueTexture(bitmap);
234}
235
237 const std::vector<SDL_Rect>& rects,
238 float max_scale, bool fit_height) {
239 if (!bitmap.is_active() || bitmap.width() <= 0 || !bitmap.texture()) {
240 ImGui::TextDisabled("%s", tr("Rendering..."));
241 return;
242 }
243 const auto available = ImGui::GetContentRegionAvail();
244 float scale =
245 std::min(max_scale, std::max(1.0f, available.x) / bitmap.width());
246 if (fit_height) {
247 scale = std::min(scale, std::max(1.0f, available.y) / bitmap.height());
248 } else {
249 scale = std::max(0.5f, scale);
250 }
251 const ImVec2 size(bitmap.width() * scale, bitmap.height() * scale);
252 const ImVec2 origin = ImGui::GetCursorScreenPos();
253 ImGui::Image((ImTextureID)(intptr_t)bitmap.texture(), size);
254 auto* draw_list = ImGui::GetWindowDrawList();
255 const ImU32 color = ImGui::GetColorU32(gui::GetWarningColor());
256 for (const auto& r : rects) {
257 draw_list->AddRect(
258 ImVec2(origin.x + r.x * scale, origin.y + r.y * scale),
259 ImVec2(origin.x + (r.x + r.w) * scale, origin.y + (r.y + r.h) * scale),
260 color, 0.0f, 1.5f);
261 }
262}
263
264void UsagePreviewView::DrawSourcePreview(bool show_controls) {
265 if (!state_ || !game_data_ || !state_->source_context ||
266 state_->source_context->owner != game_data_ ||
267 !state_->source_context->render_preview) {
268 ImGui::TextDisabled("%s", tr("No source preview available."));
269 return;
270 }
271 auto& source = *state_->source_context;
272 if (show_controls) {
273 ImGui::TextUnformatted(tr("Source preview"));
274 ImGui::SameLine();
275 ImGui::Checkbox(tr("Live"), &live_);
276 gui::AutoRegisterLastItem("checkbox", "Live source preview");
277 ImGui::SameLine();
278 if (ImGui::SmallButton(ICON_MD_REFRESH "##SourceRefresh")) {
279 source_dirty_ = true;
280 source_render_time_ = -1.0;
281 }
282 gui::AutoRegisterLastItem("button", "Refresh source preview");
283 HOVER_HINT("Render unsaved source pixels now");
284 }
285 if (!source.palette_error.empty()) {
286 // Never present an old image as a successfully refreshed source.
287 source_dirty_ = true;
288 source_render_time_ = -1.0;
289 ImGui::TextWrapped("%s", source.palette_error.c_str());
290 return;
291 }
292 const auto revision = gfx::PaletteManager::Get().ColorRevision(game_data_);
294 source_palette_revision_ != revision) {
298 source_palette_revision_ = revision;
299 source_dirty_ = true;
300 source_render_time_ = -1.0;
301 }
302 if (live_ && source.preview_revision) {
303 const auto current = source.preview_revision();
304 if (!current.ok()) {
305 source_status_ = std::string(current.status().message());
306 source_dirty_ = true;
307 source_render_time_ = -1.0;
308 ImGui::TextWrapped("%s", source_status_.c_str());
309 return;
310 }
311 if (!source_model_revision_ || *source_model_revision_ != *current) {
312 source_model_revision_ = *current;
313 source_dirty_ = true;
314 }
315 }
316 // The independent Room chooser must not invalidate the captured Source.
317 const uint64_t hash =
318 live_ ? HashWatchedSheets(/*include_room_context=*/false) : source_hash_;
319 if (hash != source_hash_) {
320 source_hash_ = hash;
321 source_dirty_ = true;
322 }
323 const double now = ImGui::GetTime();
324 if (source_dirty_ && (source_render_time_ < 0.0 ||
325 !ImGui::IsMouseDown(ImGuiMouseButton_Left) ||
327 const auto rendered = source.render_preview();
328 source_status_ = rendered.ok() ? std::string()
329 : std::string(rendered.status().message());
330 if (rendered.ok()) {
331 if (rendered->empty())
332 source_status_ = tr("Source preview is empty.");
333 else
334 ShowImage(source_bitmap_, *rendered);
335 }
336 source_dirty_ = false;
337 // Rendering may refresh source caches. Sample after that work so a
338 // renderer's own revision does not cause another render every frame.
339 if (rendered.ok() && source.preview_revision) {
340 const auto current = source.preview_revision();
341 if (current.ok())
342 source_model_revision_ = *current;
343 }
345 }
346 if (!source_status_.empty()) {
347 ImGui::TextWrapped("%s", source_status_.c_str());
348 return;
349 }
351 DrawBitmapFit(source_bitmap_, {}, 2.0f, true);
352}
353
355 const usage_preview::LiveRoomContexts& rooms) const {
356 uint64_t hash = kFnvOffset;
357 auto add = [&](uint64_t value) {
358 hash = (hash ^ value) * kFnvPrime;
359 };
361 add(state_->source_context ? state_->source_context->selected_target : 0);
364 add(gfx::PaletteManager::Get().ColorRevision(game_data_));
365 add(zelda3::CustomObjectManager::Get().asset_generation());
366 for (const auto& [id, context] : rooms) {
367 add(id);
368 add(reinterpret_cast<uintptr_t>(context.room));
369 add(context.entrance_slot);
370 if (context.room && context.room->rom() == rom_ &&
371 context.room->game_data() == game_data_) {
372 add(context.room->composite_source_revision());
373 for (const auto sheet : context.room->blocks()) {
374 add(sheet);
375 add(game_data_->sheet_store.Revision(sheet));
376 }
377 }
378 }
379 return hash;
380}
381
383 const usage_preview::LiveRoomContexts& rooms) {
384 shared_scan_ready_ = false;
385 shared_uses_.clear();
386 shared_targets_.clear();
388 if (!rom_ || !rom_->is_loaded() || !game_data_ || !state_->SourceTarget() ||
389 !state_->source_context->resolve_live_source) {
390 return absl::FailedPreconditionError(
391 "Reopen Edit Graphics from a live source to find shared uses.");
392 }
393 const auto& source = *state_->source_context;
395 RETURN_IF_ERROR(source.resolve_live_source(fresh));
396 if (fresh.owner != game_data_ ||
397 fresh.resolved.surface != source.resolved.surface ||
398 fresh.resolved.id != source.resolved.id ||
399 fresh.resolved.parent_id != source.resolved.parent_id) {
400 return absl::FailedPreconditionError(
401 "Graphics source changed; reopen Edit Graphics from the source.");
402 }
403 // Validate against the captured source without redirecting pending edits.
404 auto validated = source;
405 RETURN_IF_ERROR(UpdateGraphicsContextPalettes(validated, std::move(fresh)));
407 shared_targets_ = std::move(validated.targets);
408 } else {
409 shared_targets_.push_back(validated.targets.at(source.selected_target));
410 }
414 shared_scan_ready_ = true;
415 return absl::OkStatus();
416}
417
419 const bool expanded = ImGui::CollapsingHeader(
420 tr("Shared uses in loaded rooms"), ImGuiTreeNodeFlags_NoAutoOpenOnLog);
421 gui::AutoRegisterLastItem("header", "Shared uses in loaded rooms");
422 if (!expanded)
423 return;
424 const auto rooms = live_rooms_provider_ ? live_rooms_provider_()
427 shared_scan_ready_ = false;
428 shared_uses_.clear();
431 tr("Source or loaded rooms changed. Find shared uses again.");
432 }
433 if (ImGui::RadioButton(tr("Current tile##SharedScope"),
435 shared_whole_pattern_ = false;
436 shared_scan_ready_ = false;
438 }
439 gui::AutoRegisterLastItem("radio", "Shared uses current tile");
440 if (ImGui::RadioButton(tr("Source pattern##SharedScope"),
443 shared_scan_ready_ = false;
445 }
446 gui::AutoRegisterLastItem("radio", "Shared uses source pattern");
447 if (ImGui::Button(tr("Find shared uses"))) {
448 const auto status = ScanSharedUses(rooms);
450 status.ok() ? std::string() : std::string(status.message());
451 }
452 gui::AutoRegisterLastItem("button", "Find shared uses");
453 ImGui::TextWrapped(
454 "%s", tr("Loaded dungeon rooms only. Doors, layout, sprites, unloaded "
455 "rooms and untraced drawing are not covered. Matches are "
456 "possible uses, not a prediction of visible changes."));
457 if (!shared_scan_status_.empty())
458 ImGui::TextWrapped("%s", shared_scan_status_.c_str());
460 return;
461 size_t matches = 0, unknown = 0, failed = 0;
462 for (const auto& room : shared_uses_) {
463 matches += room.objects.size();
464 unknown += room.unknown_objects;
465 failed += !room.error.empty();
466 }
467 ImGui::Text(tr("%zu objects / %zu loaded rooms"), matches,
468 shared_uses_.size());
469 ImGui::Text(tr("%zu unknown objects; %zu rooms unavailable"), unknown,
470 failed);
471 ImGui::TextWrapped("%s", tr("Pixels = same sheet and tile. Palette = shared "
472 "source color addresses; unmapped colors are "
473 "not counted."));
474 if (ImGui::TreeNodeEx(tr("Source tiles and palettes"),
475 ImGuiTreeNodeFlags_NoAutoOpenOnLog)) {
476 for (const auto& target : shared_targets_) {
477 ImGui::Text("Sheet $%02X / tile $%02X", target.sheet_id,
478 target.tile_index);
479 if (target.writable_palette) {
480 const auto& mapping = *target.writable_palette;
481 ImGui::Text("Row %u, %s half", mapping.row,
482 mapping.half_offset == 8 ? "right" : "left");
483 for (size_t i = 1; i < mapping.colors.size(); ++i) {
484 if (const auto& address = mapping.colors[i])
485 ImGui::Text("%zu: %s / %d / %d", i,
486 zelda3::PaletteAddressGroupName(address->group),
487 address->palette_index, address->color_index);
488 }
489 } else {
490 ImGui::TextWrapped("%s", target.palette_address_limitation.c_str());
491 }
492 }
493 ImGui::TreePop();
494 }
495 if (matches == 0) {
496 ImGui::TextWrapped("%s", tr("No traced matches in these loaded rooms. "
497 "This does not establish exclusive use."));
498 }
499 ImGui::BeginChild("##SharedUseResults", ImVec2(0, 8 * ImGui::GetFontSize()),
500 true);
501 for (const auto& room : shared_uses_) {
502 if (!room.error.empty())
503 ImGui::TextWrapped("$%03X: %s", room.room_id, room.error.c_str());
504 for (const auto& object : room.objects) {
505 const auto label = absl::StrFormat(
506 "$%03X / object %03X #%zu: %s%s%s", room.room_id, object.object_id,
507 object.object_index, object.shares_pixels ? "Pixels" : "",
508 object.shares_pixels && object.shares_palette ? " + " : "",
509 object.shares_palette ? "Palette" : "");
510 if (ImGui::Selectable(
511 label.c_str(),
512 shared_preview_room_ == room.room_id &&
513 shared_preview_object_ == object.object_index)) {
514 const auto live = rooms.find(room.room_id);
515 const auto status =
516 live == rooms.end()
517 ? absl::FailedPreconditionError("Room is no longer loaded")
519 {}, false, live->second.room);
521 status.ok() ? std::string() : std::string(status.message());
522 if (status.ok()) {
523 shared_preview_room_ = room.room_id;
524 shared_preview_object_ = object.object_index;
525 QueueTexture(shared_room_preview_.mutable_bitmap());
527 }
528 }
529 gui::AutoRegisterLastItem("selectable", label);
530 if (ImGui::IsItemHovered()) {
531 ImGui::BeginTooltip();
532 for (const auto& reference : object.references)
533 ImGui::Text("Sheet $%02X / tile $%02X / row %d / slot %d",
534 reference.sheet, reference.tile_word & 0x3F,
535 (reference.tile_word >> 10) & 7, reference.slot);
536 ImGui::EndTooltip();
537 }
538 }
539 }
540 ImGui::EndChild();
541 if (shared_preview_room_ >= 0) {
542 ImGui::Text("Room $%03X / object #%zu", shared_preview_room_,
547 0.5f);
548 }
549}
550
552 if (rom_ == nullptr || !rom_->is_loaded() || game_data_ == nullptr) {
553 ImGui::TextDisabled("%s", tr("Load a ROM to preview sheet usage."));
554 return;
555 }
556 EnsureTables();
558 const auto palette_revision =
560 if (palette_revision != palette_revision_) {
561 palette_revision_ = palette_revision;
562 dirty_ = true;
563 // Cached generic renderers must reload colors too, even without a stroke.
566 }
567 const bool source_changed =
569 if (source_changed) {
571 dirty_ = true;
572 last_render_time_ = -1.0;
573 }
574 const uint16_t sheet = state_->current_sheet_id;
575 if (sheet != last_sheet_) {
576 last_sheet_ = sheet;
577 spritesets_for_sheet_.clear();
578 for (int ss = 0; ss < static_cast<int>(game_data_->spriteset_ids.size());
579 ++ss) {
580 const auto slots = zelda3::SpriteSheetSlots(
582 // Slots 4-7 are the spriteset's own sheets; 0-3 are shared by all.
583 if (std::find(slots.begin() + 4, slots.end(), sheet) != slots.end()) {
584 spritesets_for_sheet_.push_back(ss);
585 }
586 }
587 // Dungeon spritesets that a room uses come first: they have a room
588 // sprite palette to preview with.
589 std::stable_partition(
591 [this](int ss) {
592 return std::any_of(
593 room_gfx_.begin(), room_gfx_.end(), [ss](const auto& room) {
594 return room.second.spriteset + zelda3::kDungeonSpritesetBase ==
595 ss;
596 });
597 });
600 areas_for_sheet_.clear();
601 area_choice_ = 0;
602 dirty_ = true;
603 }
604
605 ImGui::Text(tr("Sheet 0x%02X"), sheet);
606 ImGui::SameLine();
607 ImGui::Checkbox(tr("Live"), &live_);
609 "Re-render while you draw (at most 10 times a second, and when the "
610 "stroke ends). Unsaved edits only; the ROM is not written.");
611 ImGui::SameLine();
612 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
613 dirty_ = true;
614 last_render_time_ = -1.0;
615 source_dirty_ = true;
616 source_render_time_ = -1.0;
617 }
618 HOVER_HINT("Render now");
619
620 // Throttle: re-render when the watched pixels changed, no more than every
621 // kMinRenderInterval, and always once the mouse is released (stroke end).
622 const uint64_t hash =
623 live_ ? HashWatchedSheets(/*include_room_context=*/mode_ == Mode::kRoom)
624 : last_hash_;
625 if (hash != last_hash_) {
626 last_hash_ = hash;
627 dirty_ = true;
628 }
629 const double now = ImGui::GetTime();
630 const bool stroke_active = ImGui::IsMouseDown(ImGuiMouseButton_Left);
631 const bool render_now =
632 dirty_ && (last_render_time_ < 0.0 || !stroke_active ||
633 now - last_render_time_ >= kMinRenderInterval);
634 if (render_now) {
635 last_render_time_ = now;
636 dirty_ = false;
637 }
638
639 if (ImGui::BeginTabBar("##UsagePreviewTabs")) {
640 if (state_->source_context && state_->source_context->render_preview &&
641 state_->source_context->owner == game_data_) {
642 if (ImGui::BeginTabItem(
643 tr("Source"), nullptr,
644 source_changed ? ImGuiTabItemFlags_SetSelected : 0)) {
645 DrawSourcePreview(false);
646 ImGui::EndTabItem();
647 }
648 }
649 if (ImGui::BeginTabItem(tr("Room"))) {
650 if (mode_ != Mode::kRoom) {
651 mode_ = Mode::kRoom;
652 dirty_ = true;
653 }
654 DrawRoomContext(render_now);
655 ImGui::EndTabItem();
656 }
657 if (ImGui::BeginTabItem(tr("Sprite"))) {
658 if (mode_ != Mode::kSprite) {
659 mode_ = Mode::kSprite;
660 dirty_ = true;
661 }
662 DrawSpriteContext(render_now, ImGui::GetIO().DeltaTime);
663 ImGui::EndTabItem();
664 }
665 if (ImGui::BeginTabItem(tr("Tile16"))) {
666 if (mode_ != Mode::kTile16) {
667 mode_ = Mode::kTile16;
668 dirty_ = true;
669 }
670 DrawTile16Context(render_now);
671 ImGui::EndTabItem();
672 }
673 ImGui::EndTabBar();
674 }
675 if (!status_.empty()) {
676 ImGui::TextColored(gui::GetWarningColor(), "%s", status_.c_str());
677 }
678}
679
680void UsagePreviewView::DrawRoomContext(bool render_now) {
681 if (rooms_for_sheet_.empty()) {
682 ImGui::TextDisabled("%s", tr("No dungeon room loads this sheet."));
683 return;
684 }
685 room_choice_ = std::clamp(room_choice_, 0,
686 static_cast<int>(rooms_for_sheet_.size()) - 1);
687 const int room_id = rooms_for_sheet_[room_choice_];
688 ImGui::SetNextItemWidth(160.0f);
689 if (ImGui::BeginCombo(tr("Room"),
690 absl::StrFormat("0x%03X", room_id).c_str())) {
691 for (int i = 0; i < static_cast<int>(rooms_for_sheet_.size()); ++i) {
692 const bool selected = i == room_choice_;
693 if (ImGui::Selectable(
694 absl::StrFormat("0x%03X", rooms_for_sheet_[i]).c_str(),
695 selected)) {
696 room_choice_ = i;
697 dirty_ = true;
698 }
699 }
700 ImGui::EndCombo();
701 }
702 ImGui::SameLine();
703 ImGui::TextDisabled(tr("%zu rooms"), rooms_for_sheet_.size());
704 if (ImGui::Checkbox(tr("Objects only"), &objects_only_)) {
705 dirty_ = true;
706 }
707 HOVER_HINT("Hide the floor and layout layers; show object layers only.");
708 ImGui::SameLine();
709 ImGui::Checkbox(tr("Outline objects using this sheet"), &outline_objects_);
710
711 if (render_now || room_preview_.room_id() != room_id) {
712 const auto live = live_rooms_.find(room_id);
713 auto* room = live == live_rooms_.end() ? nullptr : live->second.room;
714 const auto status = room_preview_.Render(rom_, game_data_, room_id,
715 Snapshot(), objects_only_, room);
716 if (room)
717 live_room_revisions_[room_id] = room->composite_source_revision();
718 status_ = status.ok() ? std::string() : std::string(status.message());
719 if (!status.ok()) {
720 object_rects_.clear();
721 return;
722 }
723 object_rects_ =
724 room_preview_.ObjectRectsUsingSheet(state_->current_sheet_id);
725 QueueTexture(room_preview_.mutable_bitmap());
726 }
727 const auto live = live_rooms_.find(room_id);
728 const auto* context = live == live_rooms_.end() ? nullptr : &live->second;
729 const auto blocks = room_preview_.blocks();
730 const bool loads_sheet = std::find(blocks.begin(), blocks.end(),
731 state_->current_sheet_id) != blocks.end();
732 ImGui::TextDisabled("%s%s", usage_preview::RoomContextLabel(context).c_str(),
733 loads_sheet ? "" : " · Sheet not loaded");
734 if (ImGui::IsItemHovered()) {
735 ImGui::BeginTooltip();
736 ImGui::TextUnformatted(
737 context ? context->provenance.c_str()
738 : "ROM room header / default entrance (not live room edits)");
739 ImGui::TextUnformatted(
740 "Session sheet pixels and graphics-group assignments");
741 ImGui::Text("%zu objects with traced references; %zu unknown/unattributed",
742 object_rects_.size(), room_preview_.unknown_objects());
743 ImGui::Text("BG sheets: %02X %02X %02X %02X %02X %02X %02X %02X", blocks[0],
744 blocks[1], blocks[2], blocks[3], blocks[4], blocks[5],
745 blocks[6], blocks[7]);
746 ImGui::TextUnformatted(
747 "Object traces only; doors, layout and sprites are unscanned.");
748 ImGui::TextUnformatted(
749 "Loaded sheets and traced references do not prove a visible pixel "
750 "change.");
751 ImGui::TextUnformatted(
752 "Coverage can be partial. Unknown includes covered, custom or untraced "
753 "drawing.");
754 int shown = 0;
755 for (const auto& use : room_preview_.tile_references()) {
756 if (use.sheet != state_->current_sheet_id)
757 continue;
758 if (++shown > 12) {
759 ImGui::TextDisabled("Additional traced references omitted here.");
760 break;
761 }
762 ImGui::Text("Object %03X: slot %d, tile %03X, palette row %d, H:%d V:%d",
763 use.object_id, use.slot, use.tile_word & 0x3FF,
764 (use.tile_word >> 10) & 7, (use.tile_word >> 14) & 1,
765 (use.tile_word >> 15) & 1);
766 }
767 ImGui::EndTooltip();
768 }
769 DrawBitmapFit(room_preview_.mutable_bitmap(),
770 outline_objects_ ? object_rects_ : std::vector<SDL_Rect>{},
771 2.0f);
772}
773
774void UsagePreviewView::DrawSpriteContext(bool render_now, float delta) {
775 if (spritesets_for_sheet_.empty()) {
776 ImGui::TextDisabled("%s",
777 tr("No spriteset loads this sheet in slots 4-7."));
778 return;
779 }
780 spriteset_choice_ = std::clamp(
781 spriteset_choice_, 0, static_cast<int>(spritesets_for_sheet_.size()) - 1);
782 const int spriteset = spritesets_for_sheet_[spriteset_choice_];
783 ImGui::SetNextItemWidth(160.0f);
784 if (ImGui::BeginCombo(tr("Spriteset"),
785 absl::StrFormat("0x%02X", spriteset).c_str())) {
786 for (int i = 0; i < static_cast<int>(spritesets_for_sheet_.size()); ++i) {
787 if (ImGui::Selectable(
788 absl::StrFormat("0x%02X", spritesets_for_sheet_[i]).c_str(),
789 i == spriteset_choice_)) {
790 spriteset_choice_ = i;
791 configured_spriteset_ = -1;
792 }
793 }
794 ImGui::EndCombo();
795 }
796
797 const bool spriteset_changed = configured_spriteset_ != spriteset;
798 if (spriteset_changed || sprite_palette_revision_ != palette_revision_) {
799 // Palette: the first dungeon room using this spriteset (header value +
800 // 0x40), else the default sprite preview rows.
801 int palette_room = -1;
802 for (const auto& [room_id, info] : room_gfx_) {
803 if (info.spriteset + zelda3::kDungeonSpritesetBase == spriteset) {
804 palette_room = room_id;
805 break;
806 }
807 }
808 const auto status = sprite_preview_.Configure(rom_, game_data_, spriteset,
809 palette_room, hack_name_);
810 status_ = status.ok() ? std::string() : std::string(status.message());
811 if (spriteset_changed) {
812 if (zsm_.has_value()) {
813 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
814 } else {
815 sprite_preview_.SetVanillaSprite(
816 static_cast<uint8_t>(vanilla_sprite_id_));
817 }
818 }
819 sprite_palette_revision_ = palette_revision_;
820 configured_spriteset_ = spriteset;
821 render_now = true;
822 }
823
824 ImGui::SetNextItemWidth(120.0f);
825 uint8_t sprite_id = static_cast<uint8_t>(vanilla_sprite_id_);
826 constexpr uint8_t kStep = 1;
827 if (ImGui::InputScalar(tr("Vanilla sprite"), ImGuiDataType_U8, &sprite_id,
828 &kStep, nullptr, "%02X",
829 ImGuiInputTextFlags_CharsHexadecimal)) {
830 vanilla_sprite_id_ = sprite_id;
831 zsm_.reset();
832 zsm_loaded_path_.clear();
833 sprite_preview_.SetVanillaSprite(static_cast<uint8_t>(vanilla_sprite_id_));
834 render_now = true;
835 }
836 ImGui::SameLine();
837 ImGui::TextDisabled("%s", zelda3::ResolveSpriteName(
838 static_cast<uint16_t>(vanilla_sprite_id_)));
839
840 ImGui::SetNextItemWidth(-80.0f);
841 ImGui::InputTextWithHint("##ZsmPath", tr(".zsm path"), zsm_path_,
842 sizeof(zsm_path_));
843 ImGui::SameLine();
844 if (ImGui::Button(tr("Load"))) {
846 const auto status = zsm.Load(zsm_path_);
847 if (status.ok()) {
848 zsm_ = std::move(zsm);
849 zsm_loaded_path_ = zsm_path_;
850 zsm_animation_ = 0;
851 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
852 status_.clear();
853 } else {
854 status_ = std::string(status.message());
855 }
856 render_now = true;
857 }
858 if (zsm_.has_value() && !zsm_->animations.empty()) {
859 zsm_animation_ = std::clamp(zsm_animation_, 0,
860 static_cast<int>(zsm_->animations.size()) - 1);
861 ImGui::SetNextItemWidth(200.0f);
862 if (ImGui::BeginCombo(
863 tr("Animation"),
864 zsm_->animations[zsm_animation_].frame_name.c_str())) {
865 for (int i = 0; i < static_cast<int>(zsm_->animations.size()); ++i) {
866 if (ImGui::Selectable(zsm_->animations[i].frame_name.c_str(),
867 i == zsm_animation_)) {
868 zsm_animation_ = i;
869 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
870 render_now = true;
871 }
872 }
873 ImGui::EndCombo();
874 }
875 if (ImGui::SmallButton(playing_ ? ICON_MD_PAUSE : ICON_MD_PLAY_ARROW)) {
876 playing_ = !playing_;
877 }
878 ImGui::SameLine();
879 if (ImGui::SmallButton(ICON_MD_SKIP_PREVIOUS)) {
880 playing_ = false;
881 sprite_preview_.SetFrame(sprite_preview_.frame() - 1);
882 render_now = true;
883 }
884 ImGui::SameLine();
885 if (ImGui::SmallButton(ICON_MD_SKIP_NEXT)) {
886 playing_ = false;
887 sprite_preview_.SetFrame(sprite_preview_.frame() + 1);
888 render_now = true;
889 }
890 ImGui::SameLine();
891 ImGui::Text(tr("Frame %d / %d"), sprite_preview_.frame(),
892 sprite_preview_.frame_count());
893 // Game speed: frame_speed ticks at 60 per second. Frame changes render
894 // immediately; the pixel data comes from the live Arena sheets, so edits
895 // show up without restarting playback.
896 if (playing_ && sprite_preview_.Advance(delta)) {
897 render_now = true;
898 }
899 } else {
900 ImGui::PushTextWrapPos(0.0f);
901 ImGui::TextDisabled(
902 "%s", tr("Vanilla sprites have one catalog pose; load a .zsm to "
903 "play its animations."));
904 ImGui::PopTextWrapPos();
905 }
906
907 if (render_now || !sprite_bitmap_.is_active()) {
908 ShowImage(sprite_bitmap_, sprite_preview_.RenderFrame(Snapshot()));
909 }
910 const auto& slots = sprite_preview_.slots();
911 ImGui::TextDisabled(
912 tr("OAM slots: %02X %02X %02X %02X | %02X %02X %02X %02X"), slots[0],
913 slots[1], slots[2], slots[3], slots[4], slots[5], slots[6], slots[7]);
914 DrawBitmapFit(sprite_bitmap_, {}, 4.0f);
915}
916
917void UsagePreviewView::DrawTile16Context(bool render_now) {
918 const uint16_t sheet = state_->current_sheet_id;
919 auto* live = live_overworld_provider_ ? live_overworld_provider_() : nullptr;
920 if (live && (!live->is_loaded() || live->rom() != rom_))
921 live = nullptr;
922 std::vector<int> choices;
923 if (live) {
924 for (const auto& map : live->overworld_maps()) {
925 if (!map.is_built() || map.game_data() != game_data_)
926 continue;
927 for (int slot = 0; slot < 8; ++slot) {
928 if (map.static_graphics(slot) == sheet) {
929 choices.push_back(map.index());
930 break;
931 }
932 }
933 }
934 const int current = live->current_map_id();
935 const auto preferred = std::find(choices.begin(), choices.end(), current);
936 if (preferred != choices.end())
937 std::rotate(choices.begin(), preferred, preferred + 1);
938 }
939 if (!areas_.has_value() && choices.empty()) {
940 ImGui::TextDisabled(
941 "%s", tr("Open an overworld area or load ROM area assignments."));
942 if (ImGui::Button(tr("Load overworld areas"))) {
943 areas_ = zelda3::CollectOverworldAreaGfx(*rom_, game_data_);
944 last_sheet_ = 0xFFFF;
945 }
946 return;
947 }
948 if (!areas_ && !choices.empty() &&
949 ImGui::SmallButton(tr("Load other ROM areas"))) {
950 areas_ = zelda3::CollectOverworldAreaGfx(*rom_, game_data_);
951 }
952 if (areas_) {
953 for (const auto& [area, info] : *areas_) {
954 if (std::find(info.static_graphics.begin(),
955 info.static_graphics.begin() + 8,
956 sheet) != info.static_graphics.begin() + 8 &&
957 std::find(choices.begin(), choices.end(), area) == choices.end()) {
958 choices.push_back(area);
959 }
960 }
961 }
962 if (choices != areas_for_sheet_) {
963 const int previous =
964 area_choice_ >= 0 &&
965 area_choice_ < static_cast<int>(areas_for_sheet_.size())
966 ? areas_for_sheet_[area_choice_]
967 : -1;
968 areas_for_sheet_ = std::move(choices);
969 const auto selected =
970 std::find(areas_for_sheet_.begin(), areas_for_sheet_.end(), previous);
971 area_choice_ = selected == areas_for_sheet_.end()
972 ? 0
973 : static_cast<int>(selected - areas_for_sheet_.begin());
974 render_now = true;
975 }
976 if (areas_for_sheet_.empty()) {
977 ImGui::TextDisabled("%s", tr("No overworld area loads this sheet."));
978 return;
979 }
980 area_choice_ = std::clamp(area_choice_, 0,
981 static_cast<int>(areas_for_sheet_.size()) - 1);
982 const int area = areas_for_sheet_[area_choice_];
983 ImGui::SetNextItemWidth(160.0f);
984 if (ImGui::BeginCombo(tr("Area"), absl::StrFormat("0x%02X", area).c_str())) {
985 for (int i = 0; i < static_cast<int>(areas_for_sheet_.size()); ++i) {
986 if (ImGui::Selectable(
987 absl::StrFormat("0x%02X", areas_for_sheet_[i]).c_str(),
988 i == area_choice_)) {
989 area_choice_ = i;
990 render_now = true;
991 }
992 }
993 ImGui::EndCombo();
994 }
995 ImGui::Checkbox(tr("Follow selection"), &follow_selection_);
996 HOVER_HINT("Use the 8x8 tile at the pixel editor's selection origin.");
997 if (!follow_selection_ || !state_->selection.is_active) {
998 ImGui::SameLine();
999 ImGui::SetNextItemWidth(100.0f);
1000 if (ImGui::InputInt(tr("8x8 tile"), &tile_index_)) {
1001 tile_index_ = std::clamp(tile_index_, 0, 63);
1002 render_now = true;
1003 }
1004 }
1005 const int tile_index = SelectedTileIndex();
1006
1007 const auto* live_map = live ? live->overworld_map(area) : nullptr;
1008 if (live_map &&
1009 (!live_map->is_built() || live_map->game_data() != game_data_))
1010 live_map = nullptr;
1011 if (live_map) {
1012 uint64_t hash = kFnvOffset;
1013 auto add = [&](uint64_t value) {
1014 hash = (hash ^ value) * kFnvPrime;
1015 };
1016 add(area);
1017 for (const auto& tile : live->tiles16())
1018 for (const auto& quadrant : tile.tiles_info)
1019 add(gfx::TileInfoToWord(quadrant));
1020 for (const auto id : live_map->tileset_key()) {
1021 add(id);
1022 add(game_data_->sheet_store.Revision(id));
1023 }
1024 for (const auto& color : live_map->current_palette())
1025 add(color.snes());
1026 if (!tile16_uses_live_source_ || tile16_preview_.area() != area ||
1027 live_tile16_hash_ != hash) {
1028 const auto status = tile16_preview_.ConfigureFromLive(
1029 *live_map, live->tiles16(), game_data_);
1030 status_ = status.ok() ? std::string() : std::string(status.message());
1031 if (!status.ok())
1032 return;
1033 tile16_uses_live_source_ = true;
1034 live_tile16_hash_ = hash;
1035 render_now = true;
1036 }
1037 ImGui::TextDisabled("%s", tr("Live overworld: unsaved Tile16 definitions"));
1038 } else {
1039 if (tile16_uses_live_source_ || tile16_preview_.area() != area) {
1040 const auto status = tile16_preview_.Configure(rom_, game_data_, area);
1041 status_ = status.ok() ? std::string() : std::string(status.message());
1042 if (!status.ok())
1043 return;
1044 tile16_uses_live_source_ = false;
1045 render_now = true;
1046 }
1047 ImGui::TextDisabled("%s",
1048 tr("ROM definitions: open this area for live edits"));
1049 }
1050 const auto tile16s = tile16_preview_.Tile16sUsingTile(sheet, tile_index);
1051 if (tile16s != tile16s_) {
1052 tile16s_ = tile16s;
1053 render_now = true;
1054 }
1055 ImGui::Text(tr("8x8 tile 0x%02X: %zu tile16s"), tile_index, tile16s_.size());
1056 if (tile16s_.empty()) {
1057 return;
1058 }
1059 if (render_now || !tile16_bitmap_.is_active()) {
1060 tile16_highlights_.clear();
1061 ShowImage(tile16_bitmap_,
1062 tile16_preview_.Render(Snapshot(), tile16s_, sheet, tile_index,
1063 &tile16_highlights_));
1064 }
1065 DrawBitmapFit(tile16_bitmap_, tile16_highlights_, 4.0f);
1066}
1067
1068} // namespace yaze::editor
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
bool is_loaded() const
Definition rom.h:199
std::optional< GraphicsEditContext > source_context
const GraphicsEditTarget * SourceTarget() const
usage_preview::SheetPixels Snapshot() const
static constexpr double kMinRenderInterval
void DrawBitmapFit(gfx::Bitmap &bitmap, const std::vector< SDL_Rect > &rects, float max_scale, bool fit_height=false)
std::optional< uint64_t > source_model_revision_
usage_preview::RoomUsagePreview shared_room_preview_
usage_preview::LiveRoomProvider live_rooms_provider_
std::optional< std::map< int, zelda3::OverworldAreaGfxInfo > > areas_
std::vector< GraphicsEditTarget > shared_targets_
void ShowImage(gfx::Bitmap &bitmap, const usage_preview::PreviewImage &image)
absl::Status ScanSharedUses(const usage_preview::LiveRoomContexts &rooms)
void DrawSourcePreview(bool show_controls=true)
uint64_t HashWatchedSheets(bool include_room_context) const
uint64_t SharedUsesHash(const usage_preview::LiveRoomContexts &rooms) const
std::map< int, uint64_t > live_room_revisions_
std::map< int, zelda3::RoomGfxInfo > room_gfx_
std::map< int, std::array< uint8_t, 16 > > room_blocks_
void SetGameData(zelda3::GameData *game_data)
usage_preview::RoomUsagePreview room_preview_
std::map< int, std::array< uint8_t, 16 > > fallback_room_blocks_
std::vector< usage_preview::LoadedRoomSourceUses > shared_uses_
usage_preview::Tile16UsagePreview tile16_preview_
std::vector< SDL_Rect > object_rects_
usage_preview::LiveRoomContexts live_rooms_
absl::Status Render(Rom *rom, zelda3::GameData *game_data, int room_id, const SheetPixels &sheets, bool objects_only, zelda3::Room *live_room=nullptr)
void Reset()
Clears the loaded room so the next Render() reloads it from the ROM.
std::vector< SDL_Rect > ObjectRectsForObject(size_t index) const
void QueueTextureCommand(TextureCommandType type, Bitmap *bitmap)
Definition arena.cc:39
static Arena & Get()
Definition arena.cc:24
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:198
TextureHandle texture() const
Definition bitmap.h:410
BitmapMetadata & metadata()
Definition bitmap.h:395
bool is_active() const
Definition bitmap.h:414
int height() const
Definition bitmap.h:399
void set_data(const std::vector< uint8_t > &data)
Definition bitmap.cc:901
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
int depth() const
Definition bitmap.h:400
uint64_t ColorRevision(const zelda3::GameData *game_data) const
static PaletteManager & Get()
Get the singleton instance.
static CustomObjectManager & Get()
uint64_t Revision(uint16_t sheet) const
#define ICON_MD_PAUSE
Definition icons.h:1389
#define ICON_MD_PLAY_ARROW
Definition icons.h:1479
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_SKIP_NEXT
Definition icons.h:1773
#define ICON_MD_SKIP_PREVIOUS
Definition icons.h:1774
#define HOVER_HINT(string)
Definition macro.h:24
uint64_t HashBytes(uint64_t hash, const std::vector< uint8_t > &bytes)
SheetPixels SnapshotSessionSheets(const std::set< uint16_t > &ids, const zelda3::GameData *game_data)
std::vector< LoadedRoomSourceUses > FindLoadedRoomSourceUses(Rom *rom, zelda3::GameData *game_data, const LiveRoomContexts &rooms, const std::vector< GraphicsEditTarget > &targets)
std::map< uint16_t, std::vector< uint8_t > > SheetPixels
Sheet id -> 4096 8bpp pixels (the GameData::graphics_buffer layout).
std::map< int, LiveRoomContext > LiveRoomContexts
std::vector< int > RoomsUsingSheet(const std::map< int, std::array< uint8_t, 16 > > &room_blocks, uint16_t sheet, bool include_sprites)
std::map< int, std::array< uint8_t, 16 > > CollectRoomSheetBlocks(Rom *rom, zelda3::GameData *game_data, const std::map< int, zelda3::RoomGfxInfo > &rooms)
Editors are the view controllers for the application.
absl::Status UpdateGraphicsContextPalettes(GraphicsEditContext &context, GraphicsEditContext refreshed)
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
const char * tr(const char *source)
Definition translator.cc:50
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
constexpr uint16_t kDungeonSpritesetBase
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
const char * PaletteAddressGroupName(PaletteAddressGroup group)
constexpr bool IsUnderworldSpriteset(int spriteset)
Dungeon rooms use spritesets 0x40-0x8F (room header value + 0x40).
std::array< uint8_t, 8 > SpriteSheetSlots(const std::array< uint8_t, 4 > &spriteset_values, bool underworld)
The eight sheets for a spriteset's four values.
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:287
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Which graphics sheets back sprite OAM tiles 0x000-0x1FF.
zelda3::ResolvedGraphicsContext resolved
absl::Status Load(const std::string &filename)
Load a ZSM file from disk.
Definition zsprite.h:139
std::array< std::array< uint8_t, 4 >, kNumSpritesets > spriteset_ids
Definition game_data.h:99
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97
Automatic widget registration helpers for ImGui Test Engine integration.