yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_matrix_census.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <utility>
7#include <vector>
8
9#include "absl/strings/str_cat.h"
10#include "absl/strings/str_format.h"
12#include "app/gui/core/icons.h"
14#include "core/features.h"
15#include "core/hack_manifest.h"
16#include "core/rom_settings.h"
17#include "imgui/imgui_internal.h"
18#include "rom/rom.h"
21
22namespace yaze::editor {
23
24namespace {
25
26// Categorical owner colors. Index 7 is purple so the eighth dungeon (Oracle
27// D8, Fortress of Secrets) matches the census PNG. Bright green and yellow
28// are left out: they mark free and reclaimable outlines.
29constexpr std::array<ImVec4, 16> kOwnerPalette = {{
30 {0.25f, 0.66f, 0.63f, 1.0f}, // teal
31 {0.88f, 0.53f, 0.23f, 1.0f}, // orange
32 {0.82f, 0.34f, 0.30f, 1.0f}, // red
33 {0.29f, 0.50f, 0.83f, 1.0f}, // blue
34 {0.42f, 0.76f, 0.88f, 1.0f}, // ice
35 {0.65f, 0.47f, 0.25f, 1.0f}, // brown
36 {0.36f, 0.42f, 0.75f, 1.0f}, // indigo
37 {0.60f, 0.36f, 0.78f, 1.0f}, // purple
38 {0.83f, 0.42f, 0.62f, 1.0f}, // pink
39 {0.69f, 0.55f, 0.42f, 1.0f}, // tan
40 {0.48f, 0.56f, 0.65f, 1.0f}, // slate
41 {0.56f, 0.23f, 0.27f, 1.0f}, // maroon
42 {0.30f, 0.52f, 0.45f, 1.0f}, // pine
43 {0.55f, 0.30f, 0.50f, 1.0f}, // plum
44 {0.20f, 0.40f, 0.55f, 1.0f}, // deep blue
45 {0.75f, 0.40f, 0.20f, 1.0f}, // rust
46}};
47constexpr ImVec4 kInteriorColor = {0.50f, 0.50f, 0.52f, 1.0f};
48constexpr ImVec4 kUnownedColor = {0.16f, 0.16f, 0.18f, 1.0f};
49constexpr float kTooltipWrapWidth = 380.0f;
50
51// Theme status colors can be muted; outlines on small cells need a clearly
52// green / yellow / red hue, so keep the theme's hue and lift saturation and
53// value.
54ImVec4 Vivid(ImVec4 color) {
55 float h = 0.0f;
56 float s = 0.0f;
57 float v = 0.0f;
58 ImGui::ColorConvertRGBtoHSV(color.x, color.y, color.z, h, s, v);
59 s = std::max(s, 0.65f);
60 v = std::max(v, 0.85f);
61 ImVec4 out(0, 0, 0, color.w);
62 ImGui::ColorConvertHSVtoRGB(h, s, v, out.x, out.y, out.z);
63 return out;
64}
65ImVec4 FreeColor() {
67}
70}
71ImVec4 OrphanColor() {
72 return Vivid(gui::GetErrorColor());
73}
74
75} // namespace
76
81
82ImVec4 RoomMatrixCensusOverlay::OwnerColor(int owner_index, bool interior) {
83 if (interior) {
84 return kInteriorColor;
85 }
86 if (owner_index < 0) {
87 return kUnownedColor;
88 }
89 return kOwnerPalette[static_cast<size_t>(owner_index) % kOwnerPalette.size()];
90}
91
93 // Work on a copy so edits and saves on the live ROM never race the
94 // background read. Project data is copied too.
96 auto custom_objects =
98 core::FeatureFlags::get().kEnableCustomObjects);
99 auto addresses = core::RomSettings::Get().address_overrides();
100 auto rom_copy = std::make_shared<Rom>(*rom);
101 std::vector<zelda3::RoomCensusOwnerGroup> owners;
102 std::set<uint8_t> warp_tags;
103 if (manifest_provider_) {
104 if (const core::HackManifest* manifest = manifest_provider_()) {
105 owners =
106 zelda3::RoomCensusOwnersFromProject(manifest->project_registry());
107 warp_tags = zelda3::RoomCensusWarpTagsFromManifest(*manifest);
108 }
109 }
111 pending_ = std::async(
112 std::launch::async,
113 [rom_copy, owners = std::move(owners), warp_tags = std::move(warp_tags),
114 custom_objects = std::move(custom_objects),
115 addresses = std::move(addresses),
116 stop_token =
117 stop_source_.get_token()]() -> absl::StatusOr<zelda3::RoomCensus> {
118 zelda3::RoomCensusOptions options;
119 options.stop_token = stop_token;
120 options.custom_objects = &custom_objects;
121 options.address_overrides = &addresses;
122 auto input = zelda3::CollectRoomCensusInput(rom_copy.get(), options);
123 if (!input.ok()) {
124 return input.status();
125 }
126 input->project_owners = owners;
127 input->warp_tag_ids = warp_tags;
128 if (stop_token.stop_requested()) {
129 return absl::CancelledError("Room census cancelled");
130 }
131 auto census = zelda3::BuildRoomCensus(*input);
132 if (stop_token.stop_requested()) {
133 return absl::CancelledError("Room census cancelled");
134 }
135 return census;
136 });
137}
138
139void RoomMatrixCensusOverlay::Update(Rom* rom) {
140 const auto generation = zelda3::CustomObjectManager::Get().asset_generation();
141 const bool custom_enabled = core::FeatureFlags::get().kEnableCustomObjects;
142 const uint64_t rom_revision =
143 rom != nullptr ? rom->object_tile_revision() : 0;
144 if (computed_for_ != rom || observed_rom_revision_ != rom_revision ||
145 observed_asset_generation_ != generation ||
146 observed_custom_enabled_ != custom_enabled) {
147 Invalidate();
148 observed_asset_generation_ = generation;
149 observed_rom_revision_ = rom_revision;
150 observed_custom_enabled_ = custom_enabled;
151 }
152 if (pending_.valid() &&
153 pending_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
154 auto result = pending_.get();
155 if (stale_) {
156 // The worker owns safe inputs, but its result no longer describes this
157 // ROM/project. Drain it before starting a replacement below.
158 census_.reset();
159 } else if (result.ok()) {
160 census_ = std::move(result).value();
161 error_.clear();
162 largest_block_.clear();
163 if (!census_->free_clusters.empty()) {
164 const auto& rooms = census_->free_clusters.front().rooms;
165 largest_block_.insert(rooms.begin(), rooms.end());
166 }
167 // Owner indices may have changed.
168 filter_owners_.clear();
169 } else {
170 census_.reset();
171 error_ = std::string(result.status().message());
172 }
173 }
174 if (!enabled_) {
175 return;
176 }
177 if (rom == nullptr || !rom->is_loaded()) {
178 census_.reset();
179 computed_for_ = nullptr;
180 return;
181 }
182 if (!pending_.valid() && (stale_ || computed_for_ != rom)) {
183 stale_ = false;
184 computed_for_ = rom;
185 StartComputation(rom);
186 }
187}
188
189void RoomMatrixCensusOverlay::WrapBeforeItem(float item_width, bool first) {
190 if (first) {
191 return;
192 }
193 const float spacing = ImGui::GetStyle().ItemSpacing.x;
194 const float right =
195 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
196 if (ImGui::GetItemRectMax().x + spacing + item_width <= right) {
197 ImGui::SameLine();
198 }
199}
200
201bool RoomMatrixCensusOverlay::DrawChip(const char* id,
202 const std::string& label_in,
203 ImVec4 swatch, bool outline_only,
204 bool active, bool first) {
205 const ImGuiStyle& style = ImGui::GetStyle();
206 const float swatch_size = ImGui::GetTextLineHeight() - 2.0f;
207 const float pad_x = style.FramePadding.x;
208 const float pad_y = 2.0f;
209 const auto* window = ImGui::GetCurrentWindow();
210 const float right =
211 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
212 // Match ItemSize's next-row origin, including indentation and columns.
213 // Remaining space on the current row must not shorten the next chip.
214 const float left =
215 window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x;
216 const float line_width = std::max(0.0f, right - left);
217 if (line_width == 0.0f) {
218 return false;
219 }
220 const ImVec2 text_size =
221 ImGui::CalcTextSize(label_in.data(), label_in.data() + label_in.size());
222 const float natural_width =
223 pad_x * 2 + swatch_size + style.ItemInnerSpacing.x + text_size.x;
224 const ImVec2 size(std::min(natural_width, line_width),
225 ImGui::GetTextLineHeight() + pad_y * 2);
226
227 WrapBeforeItem(size.x, first);
228 const ImVec2 pos = ImGui::GetCursorScreenPos();
229 const bool clicked = ImGui::InvisibleButton(id, size);
230 gui::AutoRegisterLastItem("button", absl::StrCat("Census chip ", id + 2));
231 const bool hovered = ImGui::IsItemHovered();
232
233 ImDrawList* draw_list = ImGui::GetWindowDrawList();
234 const ImVec2 max(pos.x + size.x, pos.y + size.y);
235 draw_list->PushClipRect(pos, max, true);
236 ImVec4 bg = style.Colors[active ? ImGuiCol_ButtonActive
237 : hovered ? ImGuiCol_ButtonHovered
238 : ImGuiCol_FrameBg];
239 draw_list->AddRectFilled(pos, max, ImGui::ColorConvertFloat4ToU32(bg), 4.0f);
240 if (active) {
241 draw_list->AddRect(pos, max, ImGui::GetColorU32(ImGuiCol_Text), 4.0f, 0,
242 1.5f);
243 }
244 const float fitted_pad = std::min(pad_x, size.x * 0.5f);
245 const float fitted_swatch =
246 std::min(swatch_size, std::max(0.0f, size.x - fitted_pad * 2));
247 const ImVec2 swatch_min(pos.x + fitted_pad,
248 pos.y + (size.y - swatch_size) * 0.5f);
249 const ImVec2 swatch_max(swatch_min.x + fitted_swatch,
250 swatch_min.y + swatch_size);
251 if (outline_only) {
252 draw_list->AddRectFilled(swatch_min, swatch_max,
253 ImGui::ColorConvertFloat4ToU32(kUnownedColor));
254 draw_list->AddRect(swatch_min, swatch_max,
255 ImGui::ColorConvertFloat4ToU32(swatch), 0.0f, 0, 2.0f);
256 } else {
257 draw_list->AddRectFilled(swatch_min, swatch_max,
258 ImGui::ColorConvertFloat4ToU32(swatch), 2.0f);
259 }
260 const ImVec2 text_min(
261 std::min(max.x, swatch_max.x + style.ItemInnerSpacing.x), pos.y + pad_y);
262 const ImVec2 text_max(std::max(text_min.x, max.x - fitted_pad),
263 pos.y + size.y - pad_y);
264 if (text_min.x < text_max.x) {
265 // ImGui fits the ellipsis at UTF-8 character boundaries; keep the original
266 // label intact for logging and the hover tooltip.
267 ImGui::RenderTextEllipsis(draw_list, text_min, text_max, text_max.x,
268 label_in.data(),
269 label_in.data() + label_in.size(), &text_size);
270 }
271 draw_list->PopClipRect();
272 if (hovered && natural_width > size.x) {
273 ImGui::SetTooltip("%s", label_in.c_str());
274 }
275 return clicked;
276}
277
278void RoomMatrixCensusOverlay::ToggleOwnerFilter(int owner_index) {
279 if (!filter_owners_.erase(owner_index)) {
280 filter_owners_.insert(owner_index);
281 }
282}
283
284void RoomMatrixCensusOverlay::DrawControls(Rom* rom) {
285 ImGui::PushID("RoomMatrixCensus");
286 bool on = enabled_;
287 if (ImGui::Checkbox("Census overlay", &on)) {
288 SetEnabled(on);
289 Update(rom);
290 }
291 gui::AutoRegisterLastItem("checkbox", "Census overlay");
292 if (ImGui::IsItemHovered()) {
293 ImGui::SetTooltip(
294 "Color rooms by dungeon/area; outline free (green) and reclaimable "
295 "(yellow) rooms. Same model as z3ed dungeon-room-census.");
296 }
297 if (!enabled_) {
298 ImGui::PopID();
299 return;
300 }
301 ImGui::SameLine();
302 ImGui::BeginDisabled(computing() || rom == nullptr);
303 if (ImGui::SmallButton(ICON_MD_REFRESH " Refresh")) {
304 Invalidate();
305 Update(rom);
306 }
307 gui::AutoRegisterLastItem("button", "Census Refresh");
308 ImGui::EndDisabled();
309 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
310 ImGui::SetTooltip(
311 "Recompute from the ROM (unsaved edits are not "
312 "included until saved).");
313 }
314
315 if (computing() && !census_.has_value()) {
316 ImGui::TextDisabled("Computing room census...");
317 ImGui::PopID();
318 return;
319 }
320 if (!error_.empty()) {
321 ImGui::PushTextWrapPos(0.0f);
322 ImGui::TextColored(gui::GetErrorColor(), "Census failed: %s",
323 error_.c_str());
324 ImGui::PopTextWrapPos();
325 }
326 if (!census_.has_value()) {
327 ImGui::PopID();
328 return;
329 }
330 const auto& census = *census_;
331 ImGui::PushTextWrapPos(0.0f);
332 // Info icon first: after wrapped text it could land past the right edge.
333 ImGui::TextDisabled(ICON_MD_INFO_OUTLINE);
334 if (ImGui::IsItemHovered()) {
335 ImGui::SetTooltip("Owners: %s\nVanilla baseline: %s\nEntrance table: %s",
336 census.owners_from_project ? "project dungeons.json"
337 : "derived from entrances",
338 census.has_vanilla_baseline
339 ? census.vanilla_baseline_source.c_str()
340 : "none",
341 census.expanded_entrance_tables ? "ZScream bank $0F"
342 : "vanilla $02:C813");
343 }
344 ImGui::SameLine(0.0f, 4.0f);
345 ImGui::TextWrapped("%d free, %d reclaimable, %d in use (%d orphan)%s",
346 census.free_count, census.reclaimable_count,
347 census.in_use_count, census.orphan_count,
348 computing() ? " - refreshing..." : "");
349 if (!census.free_clusters.empty()) {
350 ImGui::TextColored(FreeColor(), ICON_MD_CROP_FREE);
351 ImGui::SameLine(0.0f, 4.0f);
352 ImGui::TextWrapped("Largest free block: %s",
353 census.free_clusters.front().summary.c_str());
354 }
355 ImGui::PopTextWrapPos();
356
357 ImGui::SeparatorText("Census legend (click to filter)");
358 bool first = true;
359 if (DrawChip("##chip_free", absl::StrFormat("Free (%d)", census.free_count),
360 FreeColor(), true, filter_free_, first)) {
361 filter_free_ = !filter_free_;
362 }
363 first = false;
364 if (DrawChip("##chip_reclaimable",
365 absl::StrFormat("Reclaimable (%d)", census.reclaimable_count),
366 ReclaimableColor(), true, filter_reclaimable_, first)) {
367 filter_reclaimable_ = !filter_reclaimable_;
368 }
369 if (census.orphan_count > 0 &&
370 DrawChip("##chip_orphan",
371 absl::StrFormat("Orphan (%d)", census.orphan_count),
372 OrphanColor(), true, filter_orphan_, first)) {
373 filter_orphan_ = !filter_orphan_;
374 }
375 for (size_t i = 0; i < census.owners.size(); ++i) {
376 const auto& owner = census.owners[i];
377 if (owner.room_count == 0) {
378 continue;
379 }
380 // Short labels keep the legend to a few lines; the chip tooltip and the
381 // cell tooltip give the full name.
382 std::string label;
383 if (owner.from_project) {
384 label = absl::StrFormat("%s (%d)", owner.id, owner.room_count);
385 } else if (owner.interior || census.vanilla_owner_names) {
386 label = absl::StrFormat("%s (%d)", owner.name, owner.room_count);
387 } else {
388 label =
389 absl::StrFormat("ID 0x%02X (%d)", owner.dungeon_id, owner.room_count);
390 }
391 const std::string id = absl::StrFormat("##chip_owner_%d", i);
392 const int index = static_cast<int>(i);
393 if (DrawChip(id.c_str(), label, OwnerColor(index, owner.interior), false,
394 filter_owners_.contains(index), first)) {
395 ToggleOwnerFilter(index);
396 }
397 if (ImGui::IsItemHovered()) {
398 ImGui::SetTooltip("%s %s: %d rooms. Click to filter.", owner.id.c_str(),
399 owner.name.c_str(), owner.room_count);
400 }
401 }
402 if (HasActiveChips()) {
403 WrapBeforeItem(ImGui::CalcTextSize("Clear filters").x +
404 ImGui::GetStyle().FramePadding.x * 2,
405 false);
406 if (ImGui::SmallButton("Clear filters")) {
407 filter_free_ = filter_reclaimable_ = filter_orphan_ = false;
408 filter_owners_.clear();
409 }
410 }
411 ImGui::PopID();
412}
413
414ImU32 RoomMatrixCensusOverlay::FillColor(int room_id) const {
415 if (!census_.has_value() || room_id < 0 ||
416 room_id >= static_cast<int>(census_->rooms.size())) {
417 return ImGui::ColorConvertFloat4ToU32(kUnownedColor);
418 }
419 const auto& entry = census_->rooms[room_id];
420 ImVec4 color = OwnerColor(entry.owner_index, entry.interior);
421 if (entry.owner_index < 0) {
422 color = kUnownedColor;
423 }
424 // Keep the hex label readable on light fills.
425 color.x *= 0.78f;
426 color.y *= 0.78f;
427 color.z *= 0.78f;
428 return ImGui::ColorConvertFloat4ToU32(color);
429}
430
431std::optional<ImU32> RoomMatrixCensusOverlay::StatusOutline(int room_id) const {
432 if (!census_.has_value() || room_id < 0 ||
433 room_id >= static_cast<int>(census_->rooms.size())) {
434 return std::nullopt;
435 }
436 switch (census_->rooms[room_id].status) {
438 return ImGui::ColorConvertFloat4ToU32(FreeColor());
440 return ImGui::ColorConvertFloat4ToU32(ReclaimableColor());
442 break;
443 }
444 return std::nullopt;
445}
446
447ImU32 RoomMatrixCensusOverlay::OrphanMarkColor() {
448 return ImGui::ColorConvertFloat4ToU32(OrphanColor());
449}
450
451ImU32 RoomMatrixCensusOverlay::BlockHaloColor(float alpha) {
452 ImVec4 color = FreeColor();
453 color.w = alpha;
454 return ImGui::ColorConvertFloat4ToU32(color);
455}
456
457bool RoomMatrixCensusOverlay::InLargestFreeBlock(int room_id) const {
458 return largest_block_.contains(room_id);
459}
460
461bool RoomMatrixCensusOverlay::IsOrphan(int room_id) const {
462 return census_.has_value() && room_id >= 0 &&
463 room_id < static_cast<int>(census_->rooms.size()) &&
464 census_->rooms[room_id].orphan;
465}
466
467bool RoomMatrixCensusOverlay::MatchesChips(int room_id) const {
468 if (!HasActiveChips() || !census_.has_value()) {
469 return true;
470 }
471 if (room_id < 0 || room_id >= static_cast<int>(census_->rooms.size())) {
472 return false;
473 }
474 const auto& entry = census_->rooms[room_id];
475 return (filter_free_ && entry.status == zelda3::RoomCensusStatus::kFree) ||
476 (filter_reclaimable_ &&
478 (filter_orphan_ && entry.orphan) ||
479 filter_owners_.contains(entry.owner_index);
480}
481
482void RoomMatrixCensusOverlay::DrawTooltip(int room_id) const {
483 if (!census_.has_value() || room_id < 0 ||
484 room_id >= static_cast<int>(census_->rooms.size())) {
485 return;
486 }
487 const auto& census = *census_;
488 const auto& entry = census.rooms[room_id];
489 ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + kTooltipWrapWidth);
490 ImVec4 status_color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
491 const char* status_label = "In use";
492 switch (entry.status) {
494 status_color = FreeColor();
495 status_label = "Free";
496 break;
498 status_color = ReclaimableColor();
499 status_label = "Reclaimable";
500 break;
502 if (entry.orphan) {
503 status_color = OrphanColor();
504 status_label = "In use (orphan)";
505 }
506 break;
507 }
508 ImGui::TextColored(status_color, "Census: %s", status_label);
509 if (entry.owner_index >= 0) {
510 const auto& owner = census.owners[entry.owner_index];
511 ImGui::Text("Owner: %s%s%s", owner.interior ? "" : owner.id.c_str(),
512 owner.interior ? "" : " ", owner.name.c_str());
513 } else {
514 ImGui::TextDisabled("Owner: none");
515 }
516 if (InLargestFreeBlock(room_id)) {
517 ImGui::TextColored(FreeColor(), "In the largest free block");
518 }
519 for (const auto& reason : entry.reasons) {
520 ImGui::BulletText("%s", reason.c_str());
521 }
522 ImGui::PopTextWrapPos();
523}
524
525} // 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
uint64_t object_tile_revision() const
Definition rom.h:196
bool is_loaded() const
Definition rom.h:187
static Flags & get()
Definition features.h:119
Loads and queries the hack manifest JSON for yaze-ASM integration.
const RomAddressOverrides & address_overrides() const
static RomSettings & Get()
const zelda3::RoomCensus * census() const
std::future< absl::StatusOr< zelda3::RoomCensus > > pending_
static ImVec4 OwnerColor(int owner_index, bool interior)
StopToken get_token() const noexcept
Definition stop_token.h:38
bool request_stop() noexcept
Definition stop_token.h:40
CustomObjectRenderSnapshot CaptureRenderSnapshot(bool enabled)
static CustomObjectManager & Get()
static DrawRoutineRegistry & Get()
#define ICON_MD_INFO_OUTLINE
Definition icons.h:994
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_CROP_FREE
Definition icons.h:495
Editors are the view controllers for the application.
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
std::vector< RoomCensusOwnerGroup > RoomCensusOwnersFromProject(const core::ProjectRegistry &registry)
std::set< uint8_t > RoomCensusWarpTagsFromManifest(const core::HackManifest &manifest)
RoomCensus BuildRoomCensus(const RoomCensusInput &input)
Automatic widget registration helpers for ImGui Test Engine integration.