100 auto rom_copy = std::make_shared<Rom>(*rom);
101 std::vector<zelda3::RoomCensusOwnerGroup> owners;
102 std::set<uint8_t> warp_tags;
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),
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);
124 return input.status();
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");
132 if (stop_token.stop_requested()) {
133 return absl::CancelledError(
"Room census cancelled");
139void RoomMatrixCensusOverlay::Update(
Rom* rom) {
142 const uint64_t rom_revision =
144 if (computed_for_ != rom || observed_rom_revision_ != rom_revision ||
145 observed_asset_generation_ != generation ||
146 observed_custom_enabled_ != custom_enabled) {
148 observed_asset_generation_ = generation;
149 observed_rom_revision_ = rom_revision;
150 observed_custom_enabled_ = custom_enabled;
152 if (pending_.valid() &&
153 pending_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
154 auto result = pending_.get();
159 }
else if (result.ok()) {
160 census_ = std::move(result).value();
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());
168 filter_owners_.clear();
171 error_ = std::string(result.status().message());
177 if (rom ==
nullptr || !rom->
is_loaded()) {
179 computed_for_ =
nullptr;
182 if (!pending_.valid() && (stale_ || computed_for_ != rom)) {
185 StartComputation(rom);
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();
211 ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x;
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) {
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);
227 WrapBeforeItem(size.x, first);
228 const ImVec2 pos = ImGui::GetCursorScreenPos();
229 const bool clicked = ImGui::InvisibleButton(
id, size);
231 const bool hovered = ImGui::IsItemHovered();
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
239 draw_list->AddRectFilled(pos, max, ImGui::ColorConvertFloat4ToU32(bg), 4.0f);
241 draw_list->AddRect(pos, max, ImGui::GetColorU32(ImGuiCol_Text), 4.0f, 0,
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);
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);
257 draw_list->AddRectFilled(swatch_min, swatch_max,
258 ImGui::ColorConvertFloat4ToU32(swatch), 2.0f);
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) {
267 ImGui::RenderTextEllipsis(draw_list, text_min, text_max, text_max.x,
269 label_in.data() + label_in.size(), &text_size);
271 draw_list->PopClipRect();
272 if (hovered && natural_width > size.x) {
273 ImGui::SetTooltip(
"%s", label_in.c_str());
284void RoomMatrixCensusOverlay::DrawControls(
Rom* rom) {
285 ImGui::PushID(
"RoomMatrixCensus");
287 if (ImGui::Checkbox(
"Census overlay", &on)) {
292 if (ImGui::IsItemHovered()) {
294 "Color rooms by dungeon/area; outline free (green) and reclaimable "
295 "(yellow) rooms. Same model as z3ed dungeon-room-census.");
302 ImGui::BeginDisabled(computing() || rom ==
nullptr);
308 ImGui::EndDisabled();
309 if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
311 "Recompute from the ROM (unsaved edits are not "
312 "included until saved).");
315 if (computing() && !census_.has_value()) {
316 ImGui::TextDisabled(
"Computing room census...");
320 if (!error_.empty()) {
321 ImGui::PushTextWrapPos(0.0f);
324 ImGui::PopTextWrapPos();
326 if (!census_.has_value()) {
330 const auto& census = *census_;
331 ImGui::PushTextWrapPos(0.0f);
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()
341 census.expanded_entrance_tables ?
"ZScream bank $0F"
342 :
"vanilla $02:C813");
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()) {
351 ImGui::SameLine(0.0f, 4.0f);
352 ImGui::TextWrapped(
"Largest free block: %s",
353 census.free_clusters.front().summary.c_str());
355 ImGui::PopTextWrapPos();
357 ImGui::SeparatorText(
"Census legend (click to filter)");
359 if (DrawChip(
"##chip_free", absl::StrFormat(
"Free (%d)", census.free_count),
360 FreeColor(),
true, filter_free_, first)) {
361 filter_free_ = !filter_free_;
364 if (DrawChip(
"##chip_reclaimable",
365 absl::StrFormat(
"Reclaimable (%d)", census.reclaimable_count),
366 ReclaimableColor(),
true, filter_reclaimable_, first)) {
367 filter_reclaimable_ = !filter_reclaimable_;
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_;
375 for (
size_t i = 0; i < census.owners.size(); ++i) {
376 const auto& owner = census.owners[i];
377 if (owner.room_count == 0) {
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);
389 absl::StrFormat(
"ID 0x%02X (%d)", owner.dungeon_id, owner.room_count);
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);
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);
402 if (HasActiveChips()) {
403 WrapBeforeItem(ImGui::CalcTextSize(
"Clear filters").x +
404 ImGui::GetStyle().FramePadding.x * 2,
406 if (ImGui::SmallButton(
"Clear filters")) {
407 filter_free_ = filter_reclaimable_ = filter_orphan_ =
false;
408 filter_owners_.clear();
482void RoomMatrixCensusOverlay::DrawTooltip(
int room_id)
const {
483 if (!census_.has_value() || room_id < 0 ||
484 room_id >=
static_cast<int>(census_->rooms.size())) {
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";
498 status_color = ReclaimableColor();
499 status_label =
"Reclaimable";
503 status_color = OrphanColor();
504 status_label =
"In use (orphan)";
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());
514 ImGui::TextDisabled(
"Owner: none");
516 if (InLargestFreeBlock(room_id)) {
517 ImGui::TextColored(FreeColor(),
"In the largest free block");
519 for (
const auto& reason : entry.reasons) {
520 ImGui::BulletText(
"%s", reason.c_str());
522 ImGui::PopTextWrapPos();