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"
18
19namespace yaze::editor {
20
21namespace {
22
23constexpr uint64_t kFnvOffset = 1469598103934665603ULL;
24constexpr uint64_t kFnvPrime = 1099511628211ULL;
25
26uint64_t HashBytes(uint64_t hash, const std::vector<uint8_t>& bytes) {
27 for (const uint8_t b : bytes) {
28 hash = (hash ^ b) * kFnvPrime;
29 }
30 return hash;
31}
32
39
40} // namespace
41
43 rom_ = rom;
44 // Explicit rebinding also covers replacement at the same Rom address.
46}
47
52
54 shared_scan_ready_ = false;
55 shared_scan_status_.clear();
56 shared_targets_.clear();
57 shared_uses_.clear();
60 source_dirty_ = true;
62 tables_ready_ = false;
63 room_blocks_.clear();
65 live_rooms_.clear();
67 rooms_for_sheet_.clear();
68 object_rects_.clear();
70 group_hash_ = 0;
71 last_render_time_ = -1.0;
72 room_gfx_.clear();
73 areas_.reset();
76 last_sheet_ = 0xFFFF;
77 dirty_ = true;
78}
79
81 if (!rom_ || !rom_->is_loaded() || !game_data_)
82 return;
83 uint64_t hash = kFnvOffset;
84 auto groups = [&](const auto& table) {
85 for (const auto& row : table)
86 for (const auto id : row)
87 hash = (hash ^ id) * kFnvPrime;
88 };
92 if (tables_ready_ && group_hash_ == hash)
93 return;
94 group_hash_ = hash;
99 last_sheet_ = 0xFFFF;
100 dirty_ = true;
101 tables_ready_ = true;
102}
103
107 auto blocks = fallback_room_blocks_;
108 std::map<int, uint64_t> revisions;
109 int current = -1;
110 for (auto it = contexts.begin(); it != contexts.end();) {
111 auto* room = it->second.room;
112 if (!room || room->rom() != rom_ || room->game_data() != game_data_ ||
113 !room->IsLoaded() || room->id() != it->first) {
114 it = contexts.erase(it);
115 continue;
116 }
117 std::copy_n(room->blocks().begin(), 16, blocks[it->first].begin());
118 revisions[it->first] = room->composite_source_revision();
119 if (it->second.current)
120 current = it->first;
121 ++it;
122 }
123 const bool changed = blocks != room_blocks_ ||
124 revisions != live_room_revisions_ ||
125 current != current_live_room_;
126 if (changed || last_sheet_ != state_->current_sheet_id) {
127 const int previous =
128 room_choice_ >= 0 &&
129 room_choice_ < static_cast<int>(rooms_for_sheet_.size())
131 : -1;
134 // Keep the selected live room inspectable even if it stops loading the
135 // edited sheet; that absence is useful feedback, not a reason to hide it.
136 if (current >= 0 &&
137 std::find(rooms_for_sheet_.begin(), rooms_for_sheet_.end(), current) ==
138 rooms_for_sheet_.end())
139 rooms_for_sheet_.push_back(current);
140 const int preferred =
142 ? current
143 : previous;
144 const auto selected =
145 std::find(rooms_for_sheet_.begin(), rooms_for_sheet_.end(), preferred);
146 room_choice_ = selected == rooms_for_sheet_.end()
147 ? 0
148 : static_cast<int>(selected - rooms_for_sheet_.begin());
149 dirty_ = true;
150 last_render_time_ = -1.0;
151 }
152 room_blocks_ = std::move(blocks);
153 live_room_revisions_ = std::move(revisions);
154 live_rooms_ = std::move(contexts);
155 current_live_room_ = current;
156}
157
158uint64_t UsagePreviewView::HashWatchedSheets(bool include_room_context) const {
159 uint64_t hash = kFnvOffset;
160 // Other sheet editors can commit a sheet this room uses without adding it
161 // to GraphicsEditor's modified_sheets. Watch only the chosen room's store
162 // revisions here; membership refresh must not rasterize loaded rooms.
163 if (include_room_context && room_choice_ >= 0 &&
164 room_choice_ < static_cast<int>(rooms_for_sheet_.size())) {
165 const auto found = room_blocks_.find(rooms_for_sheet_[room_choice_]);
166 if (found != room_blocks_.end()) {
167 for (const auto sheet : found->second) {
168 hash = (hash ^ sheet) * kFnvPrime;
169 hash = (hash ^ game_data_->sheet_store.Revision(sheet)) * kFnvPrime;
170 }
171 }
172 }
173 std::set<uint16_t> ids = state_->modified_sheets;
174 ids.insert(state_->current_sheet_id);
175 if (state_->source_context) {
176 for (const auto& target : state_->source_context->targets) {
177 ids.insert(target.sheet_id);
178 }
179 for (const auto& slot : state_->source_context->resolved.slots) {
180 ids.insert(slot.sheet_id);
181 }
182 }
183 const auto sheets = usage_preview::SnapshotSessionSheets(ids, game_data_);
184 for (const auto& [id, pixels] : sheets) {
185 hash = (hash ^ id) * kFnvPrime;
186 hash = HashBytes(hash, pixels);
187 }
188 return hash;
189}
190
196
198 const auto& selection = state_->selection;
199 if (follow_selection_ && selection.is_active) {
200 const int tile = (selection.y / 8) * 16 + selection.x / 8;
201 return std::clamp(tile, 0, 63);
202 }
203 return tile_index_;
204}
205
207 const usage_preview::PreviewImage& image) {
208 if (image.empty()) {
209 return;
210 }
211 const bool indexed =
213 const int depth = indexed ? 8 : 32;
214 std::vector<SDL_Color> colors(image.colors.begin(), image.colors.end());
215 if (image.index0_transparent) {
216 colors[0].a = 0;
217 }
218 if (bitmap.width() != image.width || bitmap.height() != image.height ||
219 !bitmap.is_active() || bitmap.depth() != depth) {
220 bitmap.Create(image.width, image.height, depth, image.pixels);
221 } else {
222 bitmap.set_data(image.pixels);
223 }
224 if (indexed)
225 bitmap.SetPalette(colors);
227 QueueTexture(bitmap);
228}
229
231 const std::vector<SDL_Rect>& rects,
232 float max_scale, bool fit_height) {
233 if (!bitmap.is_active() || bitmap.width() <= 0 || !bitmap.texture()) {
234 ImGui::TextDisabled("%s", tr("Rendering..."));
235 return;
236 }
237 const auto available = ImGui::GetContentRegionAvail();
238 float scale =
239 std::min(max_scale, std::max(1.0f, available.x) / bitmap.width());
240 if (fit_height) {
241 scale = std::min(scale, std::max(1.0f, available.y) / bitmap.height());
242 } else {
243 scale = std::max(0.5f, scale);
244 }
245 const ImVec2 size(bitmap.width() * scale, bitmap.height() * scale);
246 const ImVec2 origin = ImGui::GetCursorScreenPos();
247 ImGui::Image((ImTextureID)(intptr_t)bitmap.texture(), size);
248 auto* draw_list = ImGui::GetWindowDrawList();
249 const ImU32 color = ImGui::GetColorU32(gui::GetWarningColor());
250 for (const auto& r : rects) {
251 draw_list->AddRect(
252 ImVec2(origin.x + r.x * scale, origin.y + r.y * scale),
253 ImVec2(origin.x + (r.x + r.w) * scale, origin.y + (r.y + r.h) * scale),
254 color, 0.0f, 0, 1.5f);
255 }
256}
257
258void UsagePreviewView::DrawSourcePreview(bool show_controls) {
259 if (!state_ || !game_data_ || !state_->source_context ||
260 state_->source_context->owner != game_data_ ||
261 !state_->source_context->render_preview) {
262 ImGui::TextDisabled("%s", tr("No source preview available."));
263 return;
264 }
265 auto& source = *state_->source_context;
266 if (show_controls) {
267 ImGui::TextUnformatted(tr("Source preview"));
268 ImGui::SameLine();
269 ImGui::Checkbox(tr("Live"), &live_);
270 gui::AutoRegisterLastItem("checkbox", "Live source preview");
271 ImGui::SameLine();
272 if (ImGui::SmallButton(ICON_MD_REFRESH "##SourceRefresh")) {
273 source_dirty_ = true;
274 source_render_time_ = -1.0;
275 }
276 gui::AutoRegisterLastItem("button", "Refresh source preview");
277 HOVER_HINT("Render unsaved source pixels now");
278 }
279 if (!source.palette_error.empty()) {
280 // Never present an old image as a successfully refreshed source.
281 source_dirty_ = true;
282 source_render_time_ = -1.0;
283 ImGui::TextWrapped("%s", source.palette_error.c_str());
284 return;
285 }
286 const auto revision = gfx::PaletteManager::Get().ColorRevision(game_data_);
288 source_palette_revision_ != revision) {
290 source_palette_revision_ = revision;
291 source_dirty_ = true;
292 source_render_time_ = -1.0;
293 }
294 // The independent Room chooser must not invalidate the captured Source.
295 const uint64_t hash =
296 live_ ? HashWatchedSheets(/*include_room_context=*/false) : source_hash_;
297 if (hash != source_hash_) {
298 source_hash_ = hash;
299 source_dirty_ = true;
300 }
301 const double now = ImGui::GetTime();
302 if (source_dirty_ && (source_render_time_ < 0.0 ||
303 !ImGui::IsMouseDown(ImGuiMouseButton_Left) ||
305 const auto rendered = source.render_preview();
306 source_status_ = rendered.ok() ? std::string()
307 : std::string(rendered.status().message());
308 if (rendered.ok()) {
309 if (rendered->empty())
310 source_status_ = tr("Source preview is empty.");
311 else
312 ShowImage(source_bitmap_, *rendered);
313 }
314 source_dirty_ = false;
316 }
317 if (!source_status_.empty()) {
318 ImGui::TextWrapped("%s", source_status_.c_str());
319 return;
320 }
322 DrawBitmapFit(source_bitmap_, {}, 2.0f, true);
323}
324
326 const usage_preview::LiveRoomContexts& rooms) const {
327 uint64_t hash = kFnvOffset;
328 auto add = [&](uint64_t value) {
329 hash = (hash ^ value) * kFnvPrime;
330 };
332 add(state_->source_context ? state_->source_context->selected_target : 0);
335 add(gfx::PaletteManager::Get().ColorRevision(game_data_));
336 add(zelda3::CustomObjectManager::Get().asset_generation());
337 for (const auto& [id, context] : rooms) {
338 add(id);
339 add(reinterpret_cast<uintptr_t>(context.room));
340 add(context.entrance_slot);
341 if (context.room && context.room->rom() == rom_ &&
342 context.room->game_data() == game_data_) {
343 add(context.room->composite_source_revision());
344 for (const auto sheet : context.room->blocks()) {
345 add(sheet);
346 add(game_data_->sheet_store.Revision(sheet));
347 }
348 }
349 }
350 return hash;
351}
352
354 const usage_preview::LiveRoomContexts& rooms) {
355 shared_scan_ready_ = false;
356 shared_uses_.clear();
357 shared_targets_.clear();
359 if (!rom_ || !rom_->is_loaded() || !game_data_ || !state_->SourceTarget() ||
360 !state_->source_context->resolve_live_source) {
361 return absl::FailedPreconditionError(
362 "Reopen Edit Graphics from a live source to find shared uses.");
363 }
364 const auto& source = *state_->source_context;
366 RETURN_IF_ERROR(source.resolve_live_source(fresh));
367 if (fresh.owner != game_data_ ||
368 fresh.resolved.surface != source.resolved.surface ||
369 fresh.resolved.id != source.resolved.id ||
370 fresh.resolved.parent_id != source.resolved.parent_id) {
371 return absl::FailedPreconditionError(
372 "Graphics source changed; reopen Edit Graphics from the source.");
373 }
374 // Validate against the captured source without redirecting pending edits.
375 auto validated = source;
376 RETURN_IF_ERROR(UpdateGraphicsContextPalettes(validated, std::move(fresh)));
378 shared_targets_ = std::move(validated.targets);
379 } else {
380 shared_targets_.push_back(validated.targets.at(source.selected_target));
381 }
385 shared_scan_ready_ = true;
386 return absl::OkStatus();
387}
388
390 const bool expanded = ImGui::CollapsingHeader(
391 tr("Shared uses in loaded rooms"), ImGuiTreeNodeFlags_NoAutoOpenOnLog);
392 gui::AutoRegisterLastItem("header", "Shared uses in loaded rooms");
393 if (!expanded)
394 return;
395 const auto rooms = live_rooms_provider_ ? live_rooms_provider_()
398 shared_scan_ready_ = false;
399 shared_uses_.clear();
402 tr("Source or loaded rooms changed. Find shared uses again.");
403 }
404 if (ImGui::RadioButton(tr("Current tile##SharedScope"),
406 shared_whole_pattern_ = false;
407 shared_scan_ready_ = false;
409 }
410 gui::AutoRegisterLastItem("radio", "Shared uses current tile");
411 if (ImGui::RadioButton(tr("Source pattern##SharedScope"),
414 shared_scan_ready_ = false;
416 }
417 gui::AutoRegisterLastItem("radio", "Shared uses source pattern");
418 if (ImGui::Button(tr("Find shared uses"))) {
419 const auto status = ScanSharedUses(rooms);
421 status.ok() ? std::string() : std::string(status.message());
422 }
423 gui::AutoRegisterLastItem("button", "Find shared uses");
424 ImGui::TextWrapped(
425 "%s", tr("Loaded dungeon rooms only. Doors, layout, sprites, unloaded "
426 "rooms and untraced drawing are not covered. Matches are "
427 "possible uses, not a prediction of visible changes."));
428 if (!shared_scan_status_.empty())
429 ImGui::TextWrapped("%s", shared_scan_status_.c_str());
431 return;
432 size_t matches = 0, unknown = 0, failed = 0;
433 for (const auto& room : shared_uses_) {
434 matches += room.objects.size();
435 unknown += room.unknown_objects;
436 failed += !room.error.empty();
437 }
438 ImGui::Text(tr("%zu objects / %zu loaded rooms"), matches,
439 shared_uses_.size());
440 ImGui::Text(tr("%zu unknown objects; %zu rooms unavailable"), unknown,
441 failed);
442 ImGui::TextWrapped("%s", tr("Pixels = same sheet and tile. Palette = shared "
443 "source color addresses; unmapped colors are "
444 "not counted."));
445 if (ImGui::TreeNodeEx(tr("Source tiles and palettes"),
446 ImGuiTreeNodeFlags_NoAutoOpenOnLog)) {
447 for (const auto& target : shared_targets_) {
448 ImGui::Text("Sheet $%02X / tile $%02X", target.sheet_id,
449 target.tile_index);
450 if (target.writable_palette) {
451 const auto& mapping = *target.writable_palette;
452 ImGui::Text("Row %u, %s half", mapping.row,
453 mapping.half_offset == 8 ? "right" : "left");
454 for (size_t i = 1; i < mapping.colors.size(); ++i) {
455 if (const auto& address = mapping.colors[i])
456 ImGui::Text("%zu: %s / %d / %d", i,
457 zelda3::PaletteAddressGroupName(address->group),
458 address->palette_index, address->color_index);
459 }
460 } else {
461 ImGui::TextWrapped("%s", target.palette_address_limitation.c_str());
462 }
463 }
464 ImGui::TreePop();
465 }
466 if (matches == 0) {
467 ImGui::TextWrapped("%s", tr("No traced matches in these loaded rooms. "
468 "This does not establish exclusive use."));
469 }
470 ImGui::BeginChild("##SharedUseResults", ImVec2(0, 8 * ImGui::GetFontSize()),
471 true);
472 for (const auto& room : shared_uses_) {
473 if (!room.error.empty())
474 ImGui::TextWrapped("$%03X: %s", room.room_id, room.error.c_str());
475 for (const auto& object : room.objects) {
476 const auto label = absl::StrFormat(
477 "$%03X / object %03X #%zu: %s%s%s", room.room_id, object.object_id,
478 object.object_index, object.shares_pixels ? "Pixels" : "",
479 object.shares_pixels && object.shares_palette ? " + " : "",
480 object.shares_palette ? "Palette" : "");
481 if (ImGui::Selectable(
482 label.c_str(),
483 shared_preview_room_ == room.room_id &&
484 shared_preview_object_ == object.object_index)) {
485 const auto live = rooms.find(room.room_id);
486 const auto status =
487 live == rooms.end()
488 ? absl::FailedPreconditionError("Room is no longer loaded")
490 {}, false, live->second.room);
492 status.ok() ? std::string() : std::string(status.message());
493 if (status.ok()) {
494 shared_preview_room_ = room.room_id;
495 shared_preview_object_ = object.object_index;
496 QueueTexture(shared_room_preview_.mutable_bitmap());
498 }
499 }
500 gui::AutoRegisterLastItem("selectable", label);
501 if (ImGui::IsItemHovered()) {
502 ImGui::BeginTooltip();
503 for (const auto& reference : object.references)
504 ImGui::Text("Sheet $%02X / tile $%02X / row %d / slot %d",
505 reference.sheet, reference.tile_word & 0x3F,
506 (reference.tile_word >> 10) & 7, reference.slot);
507 ImGui::EndTooltip();
508 }
509 }
510 }
511 ImGui::EndChild();
512 if (shared_preview_room_ >= 0) {
513 ImGui::Text("Room $%03X / object #%zu", shared_preview_room_,
518 0.5f);
519 }
520}
521
523 if (rom_ == nullptr || !rom_->is_loaded() || game_data_ == nullptr) {
524 ImGui::TextDisabled("%s", tr("Load a ROM to preview sheet usage."));
525 return;
526 }
527 EnsureTables();
529 const auto palette_revision =
531 if (palette_revision != palette_revision_) {
532 palette_revision_ = palette_revision;
533 dirty_ = true;
534 // Cached generic renderers must reload colors too, even without a stroke.
537 }
538 const bool source_changed =
540 if (source_changed) {
542 dirty_ = true;
543 last_render_time_ = -1.0;
544 }
545 const uint16_t sheet = state_->current_sheet_id;
546 if (sheet != last_sheet_) {
547 last_sheet_ = sheet;
548 spritesets_for_sheet_.clear();
549 for (int ss = 0; ss < static_cast<int>(game_data_->spriteset_ids.size());
550 ++ss) {
551 const auto slots = zelda3::SpriteSheetSlots(
553 // Slots 4-7 are the spriteset's own sheets; 0-3 are shared by all.
554 if (std::find(slots.begin() + 4, slots.end(), sheet) != slots.end()) {
555 spritesets_for_sheet_.push_back(ss);
556 }
557 }
558 // Dungeon spritesets that a room uses come first: they have a room
559 // sprite palette to preview with.
560 std::stable_partition(
562 [this](int ss) {
563 return std::any_of(
564 room_gfx_.begin(), room_gfx_.end(), [ss](const auto& room) {
565 return room.second.spriteset + zelda3::kDungeonSpritesetBase ==
566 ss;
567 });
568 });
571 areas_for_sheet_.clear();
572 area_choice_ = 0;
573 dirty_ = true;
574 }
575
576 ImGui::Text(tr("Sheet 0x%02X"), sheet);
577 ImGui::SameLine();
578 ImGui::Checkbox(tr("Live"), &live_);
580 "Re-render while you draw (at most 10 times a second, and when the "
581 "stroke ends). Unsaved edits only; the ROM is not written.");
582 ImGui::SameLine();
583 if (ImGui::SmallButton(ICON_MD_REFRESH)) {
584 dirty_ = true;
585 last_render_time_ = -1.0;
586 source_dirty_ = true;
587 source_render_time_ = -1.0;
588 }
589 HOVER_HINT("Render now");
590
591 // Throttle: re-render when the watched pixels changed, no more than every
592 // kMinRenderInterval, and always once the mouse is released (stroke end).
593 const uint64_t hash =
594 live_ ? HashWatchedSheets(/*include_room_context=*/mode_ == Mode::kRoom)
595 : last_hash_;
596 if (hash != last_hash_) {
597 last_hash_ = hash;
598 dirty_ = true;
599 }
600 const double now = ImGui::GetTime();
601 const bool stroke_active = ImGui::IsMouseDown(ImGuiMouseButton_Left);
602 const bool render_now =
603 dirty_ && (last_render_time_ < 0.0 || !stroke_active ||
604 now - last_render_time_ >= kMinRenderInterval);
605 if (render_now) {
606 last_render_time_ = now;
607 dirty_ = false;
608 }
609
610 if (ImGui::BeginTabBar("##UsagePreviewTabs")) {
611 if (state_->source_context && state_->source_context->render_preview &&
612 state_->source_context->owner == game_data_) {
613 if (ImGui::BeginTabItem(
614 tr("Source"), nullptr,
615 source_changed ? ImGuiTabItemFlags_SetSelected : 0)) {
616 DrawSourcePreview(false);
617 ImGui::EndTabItem();
618 }
619 }
620 if (ImGui::BeginTabItem(tr("Room"))) {
621 if (mode_ != Mode::kRoom) {
622 mode_ = Mode::kRoom;
623 dirty_ = true;
624 }
625 DrawRoomContext(render_now);
626 ImGui::EndTabItem();
627 }
628 if (ImGui::BeginTabItem(tr("Sprite"))) {
629 if (mode_ != Mode::kSprite) {
630 mode_ = Mode::kSprite;
631 dirty_ = true;
632 }
633 DrawSpriteContext(render_now, ImGui::GetIO().DeltaTime);
634 ImGui::EndTabItem();
635 }
636 if (ImGui::BeginTabItem(tr("Tile16"))) {
637 if (mode_ != Mode::kTile16) {
638 mode_ = Mode::kTile16;
639 dirty_ = true;
640 }
641 DrawTile16Context(render_now);
642 ImGui::EndTabItem();
643 }
644 ImGui::EndTabBar();
645 }
646 if (!status_.empty()) {
647 ImGui::TextColored(gui::GetWarningColor(), "%s", status_.c_str());
648 }
649}
650
651void UsagePreviewView::DrawRoomContext(bool render_now) {
652 if (rooms_for_sheet_.empty()) {
653 ImGui::TextDisabled("%s", tr("No dungeon room loads this sheet."));
654 return;
655 }
656 room_choice_ = std::clamp(room_choice_, 0,
657 static_cast<int>(rooms_for_sheet_.size()) - 1);
658 const int room_id = rooms_for_sheet_[room_choice_];
659 ImGui::SetNextItemWidth(160.0f);
660 if (ImGui::BeginCombo(tr("Room"),
661 absl::StrFormat("0x%03X", room_id).c_str())) {
662 for (int i = 0; i < static_cast<int>(rooms_for_sheet_.size()); ++i) {
663 const bool selected = i == room_choice_;
664 if (ImGui::Selectable(
665 absl::StrFormat("0x%03X", rooms_for_sheet_[i]).c_str(),
666 selected)) {
667 room_choice_ = i;
668 dirty_ = true;
669 }
670 }
671 ImGui::EndCombo();
672 }
673 ImGui::SameLine();
674 ImGui::TextDisabled(tr("%zu rooms"), rooms_for_sheet_.size());
675 if (ImGui::Checkbox(tr("Objects only"), &objects_only_)) {
676 dirty_ = true;
677 }
678 HOVER_HINT("Hide the floor and layout layers; show object layers only.");
679 ImGui::SameLine();
680 ImGui::Checkbox(tr("Outline objects using this sheet"), &outline_objects_);
681
682 if (render_now || room_preview_.room_id() != room_id) {
683 const auto live = live_rooms_.find(room_id);
684 auto* room = live == live_rooms_.end() ? nullptr : live->second.room;
685 const auto status = room_preview_.Render(rom_, game_data_, room_id,
686 Snapshot(), objects_only_, room);
687 if (room)
688 live_room_revisions_[room_id] = room->composite_source_revision();
689 status_ = status.ok() ? std::string() : std::string(status.message());
690 if (!status.ok()) {
691 object_rects_.clear();
692 return;
693 }
694 object_rects_ =
695 room_preview_.ObjectRectsUsingSheet(state_->current_sheet_id);
696 QueueTexture(room_preview_.mutable_bitmap());
697 }
698 const auto live = live_rooms_.find(room_id);
699 const auto* context = live == live_rooms_.end() ? nullptr : &live->second;
700 const auto blocks = room_preview_.blocks();
701 const bool loads_sheet = std::find(blocks.begin(), blocks.end(),
702 state_->current_sheet_id) != blocks.end();
703 ImGui::TextDisabled("%s%s", usage_preview::RoomContextLabel(context).c_str(),
704 loads_sheet ? "" : " · Sheet not loaded");
705 if (ImGui::IsItemHovered()) {
706 ImGui::BeginTooltip();
707 ImGui::TextUnformatted(
708 context ? context->provenance.c_str()
709 : "ROM room header / default entrance (not live room edits)");
710 ImGui::TextUnformatted(
711 "Session sheet pixels and graphics-group assignments");
712 ImGui::Text("%zu objects with traced references; %zu unknown/unattributed",
713 object_rects_.size(), room_preview_.unknown_objects());
714 ImGui::Text("BG sheets: %02X %02X %02X %02X %02X %02X %02X %02X", blocks[0],
715 blocks[1], blocks[2], blocks[3], blocks[4], blocks[5],
716 blocks[6], blocks[7]);
717 ImGui::TextUnformatted(
718 "Object traces only; doors, layout and sprites are unscanned.");
719 ImGui::TextUnformatted(
720 "Loaded sheets and traced references do not prove a visible pixel "
721 "change.");
722 ImGui::TextUnformatted(
723 "Coverage can be partial. Unknown includes covered, custom or untraced "
724 "drawing.");
725 int shown = 0;
726 for (const auto& use : room_preview_.tile_references()) {
727 if (use.sheet != state_->current_sheet_id)
728 continue;
729 if (++shown > 12) {
730 ImGui::TextDisabled("Additional traced references omitted here.");
731 break;
732 }
733 ImGui::Text("Object %03X: slot %d, tile %03X, palette row %d, H:%d V:%d",
734 use.object_id, use.slot, use.tile_word & 0x3FF,
735 (use.tile_word >> 10) & 7, (use.tile_word >> 14) & 1,
736 (use.tile_word >> 15) & 1);
737 }
738 ImGui::EndTooltip();
739 }
740 DrawBitmapFit(room_preview_.mutable_bitmap(),
741 outline_objects_ ? object_rects_ : std::vector<SDL_Rect>{},
742 2.0f);
743}
744
745void UsagePreviewView::DrawSpriteContext(bool render_now, float delta) {
746 if (spritesets_for_sheet_.empty()) {
747 ImGui::TextDisabled("%s",
748 tr("No spriteset loads this sheet in slots 4-7."));
749 return;
750 }
751 spriteset_choice_ = std::clamp(
752 spriteset_choice_, 0, static_cast<int>(spritesets_for_sheet_.size()) - 1);
753 const int spriteset = spritesets_for_sheet_[spriteset_choice_];
754 ImGui::SetNextItemWidth(160.0f);
755 if (ImGui::BeginCombo(tr("Spriteset"),
756 absl::StrFormat("0x%02X", spriteset).c_str())) {
757 for (int i = 0; i < static_cast<int>(spritesets_for_sheet_.size()); ++i) {
758 if (ImGui::Selectable(
759 absl::StrFormat("0x%02X", spritesets_for_sheet_[i]).c_str(),
760 i == spriteset_choice_)) {
761 spriteset_choice_ = i;
762 configured_spriteset_ = -1;
763 }
764 }
765 ImGui::EndCombo();
766 }
767
768 const bool spriteset_changed = configured_spriteset_ != spriteset;
769 if (spriteset_changed || sprite_palette_revision_ != palette_revision_) {
770 // Palette: the first dungeon room using this spriteset (header value +
771 // 0x40), else the default sprite preview rows.
772 int palette_room = -1;
773 for (const auto& [room_id, info] : room_gfx_) {
774 if (info.spriteset + zelda3::kDungeonSpritesetBase == spriteset) {
775 palette_room = room_id;
776 break;
777 }
778 }
779 const auto status = sprite_preview_.Configure(rom_, game_data_, spriteset,
780 palette_room, hack_name_);
781 status_ = status.ok() ? std::string() : std::string(status.message());
782 if (spriteset_changed) {
783 if (zsm_.has_value()) {
784 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
785 } else {
786 sprite_preview_.SetVanillaSprite(
787 static_cast<uint8_t>(vanilla_sprite_id_));
788 }
789 }
790 sprite_palette_revision_ = palette_revision_;
791 configured_spriteset_ = spriteset;
792 render_now = true;
793 }
794
795 ImGui::SetNextItemWidth(120.0f);
796 uint8_t sprite_id = static_cast<uint8_t>(vanilla_sprite_id_);
797 constexpr uint8_t kStep = 1;
798 if (ImGui::InputScalar(tr("Vanilla sprite"), ImGuiDataType_U8, &sprite_id,
799 &kStep, nullptr, "%02X",
800 ImGuiInputTextFlags_CharsHexadecimal)) {
801 vanilla_sprite_id_ = sprite_id;
802 zsm_.reset();
803 zsm_loaded_path_.clear();
804 sprite_preview_.SetVanillaSprite(static_cast<uint8_t>(vanilla_sprite_id_));
805 render_now = true;
806 }
807 ImGui::SameLine();
808 ImGui::TextDisabled("%s", zelda3::ResolveSpriteName(
809 static_cast<uint16_t>(vanilla_sprite_id_)));
810
811 ImGui::SetNextItemWidth(-80.0f);
812 ImGui::InputTextWithHint("##ZsmPath", tr(".zsm path"), zsm_path_,
813 sizeof(zsm_path_));
814 ImGui::SameLine();
815 if (ImGui::Button(tr("Load"))) {
817 const auto status = zsm.Load(zsm_path_);
818 if (status.ok()) {
819 zsm_ = std::move(zsm);
820 zsm_loaded_path_ = zsm_path_;
821 zsm_animation_ = 0;
822 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
823 status_.clear();
824 } else {
825 status_ = std::string(status.message());
826 }
827 render_now = true;
828 }
829 if (zsm_.has_value() && !zsm_->animations.empty()) {
830 zsm_animation_ = std::clamp(zsm_animation_, 0,
831 static_cast<int>(zsm_->animations.size()) - 1);
832 ImGui::SetNextItemWidth(200.0f);
833 if (ImGui::BeginCombo(
834 tr("Animation"),
835 zsm_->animations[zsm_animation_].frame_name.c_str())) {
836 for (int i = 0; i < static_cast<int>(zsm_->animations.size()); ++i) {
837 if (ImGui::Selectable(zsm_->animations[i].frame_name.c_str(),
838 i == zsm_animation_)) {
839 zsm_animation_ = i;
840 sprite_preview_.SetZsm(*zsm_, zsm_animation_);
841 render_now = true;
842 }
843 }
844 ImGui::EndCombo();
845 }
846 if (ImGui::SmallButton(playing_ ? ICON_MD_PAUSE : ICON_MD_PLAY_ARROW)) {
847 playing_ = !playing_;
848 }
849 ImGui::SameLine();
850 if (ImGui::SmallButton(ICON_MD_SKIP_PREVIOUS)) {
851 playing_ = false;
852 sprite_preview_.SetFrame(sprite_preview_.frame() - 1);
853 render_now = true;
854 }
855 ImGui::SameLine();
856 if (ImGui::SmallButton(ICON_MD_SKIP_NEXT)) {
857 playing_ = false;
858 sprite_preview_.SetFrame(sprite_preview_.frame() + 1);
859 render_now = true;
860 }
861 ImGui::SameLine();
862 ImGui::Text(tr("Frame %d / %d"), sprite_preview_.frame(),
863 sprite_preview_.frame_count());
864 // Game speed: frame_speed ticks at 60 per second. Frame changes render
865 // immediately; the pixel data comes from the live Arena sheets, so edits
866 // show up without restarting playback.
867 if (playing_ && sprite_preview_.Advance(delta)) {
868 render_now = true;
869 }
870 } else {
871 ImGui::PushTextWrapPos(0.0f);
872 ImGui::TextDisabled(
873 "%s", tr("Vanilla sprites have one catalog pose; load a .zsm to "
874 "play its animations."));
875 ImGui::PopTextWrapPos();
876 }
877
878 if (render_now || !sprite_bitmap_.is_active()) {
879 ShowImage(sprite_bitmap_, sprite_preview_.RenderFrame(Snapshot()));
880 }
881 const auto& slots = sprite_preview_.slots();
882 ImGui::TextDisabled(
883 tr("OAM slots: %02X %02X %02X %02X | %02X %02X %02X %02X"), slots[0],
884 slots[1], slots[2], slots[3], slots[4], slots[5], slots[6], slots[7]);
885 DrawBitmapFit(sprite_bitmap_, {}, 4.0f);
886}
887
888void UsagePreviewView::DrawTile16Context(bool render_now) {
889 const uint16_t sheet = state_->current_sheet_id;
890 if (!areas_.has_value()) {
891 ImGui::TextDisabled(
892 "%s", tr("Reads all 160 overworld areas' graphics (about a second)."));
893 if (ImGui::Button(tr("Load overworld areas"))) {
894 areas_ = zelda3::CollectOverworldAreaGfx(*rom_, game_data_);
895 last_sheet_ = 0xFFFF; // recompute the per-sheet lists
896 }
897 return;
898 }
899 if (areas_for_sheet_.empty()) {
900 for (const auto& [area, info] : *areas_) {
901 if (std::find(info.static_graphics.begin(),
902 info.static_graphics.begin() + 8,
903 sheet) != info.static_graphics.begin() + 8) {
904 areas_for_sheet_.push_back(area);
905 }
906 }
907 }
908 if (areas_for_sheet_.empty()) {
909 ImGui::TextDisabled("%s", tr("No overworld area loads this sheet."));
910 return;
911 }
912 area_choice_ = std::clamp(area_choice_, 0,
913 static_cast<int>(areas_for_sheet_.size()) - 1);
914 const int area = areas_for_sheet_[area_choice_];
915 ImGui::SetNextItemWidth(160.0f);
916 if (ImGui::BeginCombo(tr("Area"), absl::StrFormat("0x%02X", area).c_str())) {
917 for (int i = 0; i < static_cast<int>(areas_for_sheet_.size()); ++i) {
918 if (ImGui::Selectable(
919 absl::StrFormat("0x%02X", areas_for_sheet_[i]).c_str(),
920 i == area_choice_)) {
921 area_choice_ = i;
922 render_now = true;
923 }
924 }
925 ImGui::EndCombo();
926 }
927 ImGui::Checkbox(tr("Follow selection"), &follow_selection_);
928 HOVER_HINT("Use the 8x8 tile at the pixel editor's selection origin.");
929 if (!follow_selection_ || !state_->selection.is_active) {
930 ImGui::SameLine();
931 ImGui::SetNextItemWidth(100.0f);
932 if (ImGui::InputInt(tr("8x8 tile"), &tile_index_)) {
933 tile_index_ = std::clamp(tile_index_, 0, 63);
934 render_now = true;
935 }
936 }
937 const int tile_index = SelectedTileIndex();
938
939 if (tile16_preview_.area() != area) {
940 const auto status = tile16_preview_.Configure(rom_, game_data_, area);
941 status_ = status.ok() ? std::string() : std::string(status.message());
942 render_now = true;
943 }
944 const auto tile16s = tile16_preview_.Tile16sUsingTile(sheet, tile_index);
945 if (tile16s != tile16s_) {
946 tile16s_ = tile16s;
947 render_now = true;
948 }
949 ImGui::Text(tr("8x8 tile 0x%02X: %zu tile16s"), tile_index, tile16s_.size());
950 if (tile16s_.empty()) {
951 return;
952 }
953 if (render_now || !tile16_bitmap_.is_active()) {
954 tile16_highlights_.clear();
955 ShowImage(tile16_bitmap_,
956 tile16_preview_.Render(Snapshot(), tile16s_, sheet, tile_index,
957 &tile16_highlights_));
958 }
959 DrawBitmapFit(tile16_bitmap_, tile16_highlights_, 4.0f);
960}
961
962} // 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:49
bool is_loaded() const
Definition rom.h:187
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)
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)
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.