yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_census.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <cstdlib>
6#include <deque>
7#include <functional>
8#include <map>
9#include <utility>
10
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_join.h"
13#include "core/hack_manifest.h"
14#include "rom/rom.h"
18#include "zelda3/dungeon/room.h"
23
24namespace yaze::zelda3 {
25
26namespace {
27
28constexpr int kRooms = kRoomCensusRoomCount;
29// Free-room blocks: a contiguous core of at least kFreeBlockMinCore rooms
30// takes in smaller free cores within kFreeBlockMergeDistance grid cells
31// (Chebyshev distance, same $A1 page).
32constexpr int kFreeBlockMinCore = 3;
33constexpr int kFreeBlockMergeDistance = 3;
34
35bool IsEncodedStreamObject(const RoomObject& object) {
36 return (object.options() & ObjectOption::Torch) == ObjectOption::Nothing &&
37 (object.options() & ObjectOption::Block) == ObjectOption::Nothing;
38}
39
40std::string RoomHex(int room_id) {
41 return absl::StrFormat("0x%02X", room_id);
42}
43
44std::string VanillaDungeonName(int dungeon_id) {
45 switch (dungeon_id & 0xFE) {
46 case 0x00:
47 return "Sewers";
48 case 0x02:
49 return "Hyrule Castle";
50 case 0x04:
51 return "Eastern Palace";
52 case 0x06:
53 return "Desert Palace";
54 case 0x08:
55 return "Agahnim's Tower";
56 case 0x0A:
57 return "Swamp Palace";
58 case 0x0C:
59 return "Palace of Darkness";
60 case 0x0E:
61 return "Misery Mire";
62 case 0x10:
63 return "Skull Woods";
64 case 0x12:
65 return "Ice Palace";
66 case 0x14:
67 return "Tower of Hera";
68 case 0x16:
69 return "Thieves' Town";
70 case 0x18:
71 return "Turtle Rock";
72 case 0x1A:
73 return "Ganon's Tower";
74 }
75 return "";
76}
77
78std::vector<uint16_t> FingerprintRoom(const Room& room) {
79 std::vector<uint16_t> hashes;
80 for (const auto& object : room.GetTileObjects()) {
81 if (!IsEncodedStreamObject(object)) {
82 continue;
83 }
84 hashes.push_back(HashRoomCensusObject(object.id_, object.x(), object.y(),
85 object.size()));
86 }
87 std::sort(hashes.begin(), hashes.end());
88 return hashes;
89}
90
91} // namespace
92
94 switch (status) {
96 return "free";
98 return "reclaimable";
100 return "in_use";
101 }
102 return "in_use";
103}
104
105uint16_t HashRoomCensusObject(int id, int x, int y, int size) {
106 uint32_t key = ((static_cast<uint32_t>(id) & 0xFFFu) << 20) |
107 ((static_cast<uint32_t>(x) & 0x3Fu) << 14) |
108 ((static_cast<uint32_t>(y) & 0x3Fu) << 8) |
109 (static_cast<uint32_t>(size) & 0xFFu);
110 key ^= 0x5A17C3E5u;
111 key *= 0x9E3779B1u;
112 key ^= key >> 15;
113 key *= 0x85EBCA77u;
114 key ^= key >> 13;
115 return static_cast<uint16_t>(key >> 16);
116}
117
118float RoomFingerprintSimilarity(const std::vector<uint16_t>& a,
119 const std::vector<uint16_t>& b) {
120 // Multiset Jaccard over sorted inputs.
121 if (a.empty() && b.empty()) {
122 return 1.0f;
123 }
124 size_t i = 0;
125 size_t j = 0;
126 size_t common = 0;
127 while (i < a.size() && j < b.size()) {
128 if (a[i] == b[j]) {
129 ++common;
130 ++i;
131 ++j;
132 } else if (a[i] < b[j]) {
133 ++i;
134 } else {
135 ++j;
136 }
137 }
138 const size_t total = a.size() + b.size() - common;
139 return total == 0 ? 1.0f
140 : static_cast<float>(common) / static_cast<float>(total);
141}
142
143absl::StatusOr<std::vector<std::vector<uint16_t>>> ComputeRoomFingerprints(
144 Rom* rom, util::StopToken stop_token) {
145 if (stop_token.stop_requested()) {
146 return absl::CancelledError("Room census cancelled");
147 }
148 if (rom == nullptr || !rom->is_loaded()) {
149 return absl::FailedPreconditionError("ROM is not loaded");
150 }
151 std::vector<std::vector<uint16_t>> out(kRooms);
152 for (int room_id = 0; room_id < kRooms; ++room_id) {
153 if (stop_token.stop_requested()) {
154 return absl::CancelledError("Room census cancelled");
155 }
156 Room room = LoadRoomFromRom(rom, room_id);
157 out[room_id] = FingerprintRoom(room);
158 }
159 if (stop_token.stop_requested()) {
160 return absl::CancelledError("Room census cancelled");
161 }
162 return out;
163}
164
165std::vector<RoomCensusOwnerGroup> RoomCensusOwnersFromProject(
166 const core::ProjectRegistry& registry) {
167 std::vector<RoomCensusOwnerGroup> groups;
168 for (const auto& dungeon : registry.dungeons) {
170 group.id = dungeon.id;
171 group.name = dungeon.name.empty() ? dungeon.id : dungeon.name;
172 for (const auto& room : dungeon.rooms) {
173 if (room.id >= 0 && room.id < kRooms) {
174 group.rooms.push_back(room.id);
175 }
176 }
177 if (!group.rooms.empty()) {
178 groups.push_back(std::move(group));
179 }
180 }
181 return groups;
182}
183
185 const core::HackManifest& manifest) {
186 std::set<uint8_t> tags;
187 for (const auto& tag : manifest.room_tags()) {
188 if (!tag.enabled) {
189 continue;
190 }
191 std::string lowered = tag.name;
192 std::transform(lowered.begin(), lowered.end(), lowered.begin(),
193 [](unsigned char c) { return std::tolower(c); });
194 if (lowered.find("warptag") != std::string::npos || lowered == "warp_tag") {
195 tags.insert(tag.tag_id);
196 }
197 }
198 return tags;
199}
200
201absl::StatusOr<RoomCensusInput> CollectRoomCensusInput(
202 Rom* rom, const RoomCensusOptions& options) {
203 if (options.stop_token.stop_requested()) {
204 return absl::CancelledError("Room census cancelled");
205 }
206 if (rom == nullptr || !rom->is_loaded()) {
207 return absl::FailedPreconditionError("ROM is not loaded");
208 }
209 RoomCensusInput input;
210 input.rooms.resize(kRooms);
211
212 PitDamageTable pit_table;
213 const bool has_pit_table = PitDamageTable::LoadFromRom(rom, &pit_table).ok();
214
215 std::vector<std::vector<uint16_t>> vanilla_from_rom;
216 if (options.vanilla_rom != nullptr) {
217 auto fingerprints =
219 if (!fingerprints.ok()) {
220 return fingerprints.status();
221 }
222 vanilla_from_rom = std::move(fingerprints).value();
223 input.has_vanilla_baseline = true;
225 "vanilla ROM " + options.vanilla_rom->filename();
227 input.has_vanilla_baseline = true;
228 input.vanilla_baseline_source = "built-in US 1.0 fingerprints";
229 }
230
231 for (int room_id = 0; room_id < kRooms; ++room_id) {
232 if (options.stop_token.stop_requested()) {
233 return absl::CancelledError("Room census cancelled");
234 }
235 Room room = LoadRoomFromRom(rom, room_id);
236 room.LoadSprites();
237 RoomCensusRoomFacts& facts = input.rooms[room_id];
239 auto links,
240 CollectRoomLinkFacts(rom, room, has_pit_table ? &pit_table : nullptr,
241 options.custom_objects, options.stop_token));
242 static_cast<RoomLinkFacts&>(facts) = std::move(links);
243 facts.sprite_count = static_cast<int>(room.GetSprites().size());
244 facts.chest_count = static_cast<int>(room.GetChests().size());
245 for (const auto& object : room.GetTileObjects()) {
246 facts.object_count += IsEncodedStreamObject(object) ? 1 : 0;
247 }
248
249 if (options.room_visitor) {
250 options.room_visitor(room_id, room);
251 }
252
253 if (input.has_vanilla_baseline) {
254 const auto fingerprint = FingerprintRoom(room);
255 const std::vector<uint16_t> vanilla =
256 !vanilla_from_rom.empty() ? vanilla_from_rom[room_id]
258 facts.vanilla_similarity =
259 RoomFingerprintSimilarity(fingerprint, vanilla);
260 }
261 }
262
263 // Entrances. The game reads ZScream's bank $0F tables when present.
264 if (options.stop_token.stop_requested()) {
265 return absl::CancelledError("Room census cancelled");
266 }
267 ASSIGN_OR_RETURN(auto entrance_layout, ResolveEntranceLayout(rom->vector()));
269 entrance_layout.kind == EntranceLayoutKind::kZscreamExpanded;
270 std::set<int> placed_entrances;
271 auto add_entrance = [&](int entrance_id, RoomReferenceKind kind,
272 std::string placement, int ow_map) {
273 const auto entry = ReadDungeonEntranceTarget(*rom, entrance_id);
274 if (!entry.has_value() || !IsDungeonRoomId(entry->room_id)) {
275 return;
276 }
278 fact.entrance_id = entrance_id;
279 fact.room_id = entry->room_id;
280 fact.dungeon_id = entry->dungeon_id;
281 fact.kind = kind;
282 fact.placement = std::move(placement);
283 fact.ow_map = ow_map;
284 input.entrances.push_back(std::move(fact));
285 };
286 auto entrances = options.address_overrides != nullptr
287 ? LoadEntrances(rom, *options.address_overrides)
288 : LoadEntrances(rom);
289 if (entrances.ok()) {
290 for (const auto& entrance : *entrances) {
291 if (entrance.map_pos_ == 0xFFFF) {
292 continue;
293 }
294 placed_entrances.insert(entrance.entrance_id_);
295 add_entrance(entrance.entrance_id_, RoomReferenceKind::kEntrance,
296 absl::StrFormat("OW 0x%02X", entrance.map_id_ & 0xFF),
297 entrance.map_id_ & 0xFF);
298 }
299 }
300 if (auto holes = LoadHoles(rom); holes.ok()) {
301 for (const auto& hole : *holes) {
302 placed_entrances.insert(hole.entrance_id_);
303 add_entrance(hole.entrance_id_, RoomReferenceKind::kHole,
304 absl::StrFormat("OW 0x%02X", hole.map_id_ & 0xFF),
305 hole.map_id_ & 0xFF);
306 }
307 }
308 for (int entrance_id = 0; entrance_id < kDungeonEntranceCount;
309 ++entrance_id) {
310 if (!placed_entrances.contains(entrance_id)) {
311 add_entrance(entrance_id, RoomReferenceKind::kUnplacedEntrance, "", -1);
312 }
313 }
314 for (int spawn_id = 0; spawn_id < kNumDungeonSpawnPoints; ++spawn_id) {
315 auto spawn = DungeonSpawnPoint::Load(*rom, spawn_id);
316 if (!spawn.ok() || !IsDungeonRoomId(spawn->room_id)) {
317 continue;
318 }
320 fact.entrance_id = spawn_id;
321 fact.room_id = spawn->room_id;
322 fact.dungeon_id = spawn->dungeon_id;
324 input.entrances.push_back(std::move(fact));
325 }
326
327 if (options.stop_token.stop_requested()) {
328 return absl::CancelledError("Room census cancelled");
329 }
330 {
331 DungeonMapLabels labels;
332 if (auto maps = LoadDungeonMaps(*rom, labels); maps.ok()) {
333 for (const auto& map : *maps) {
334 std::vector<int> rooms;
335 for (const auto& floor : map.floor_rooms) {
336 for (uint8_t room : floor) {
337 // 0x0F marks an empty map cell.
338 if (room != 0x0F &&
339 std::find(rooms.begin(), rooms.end(), room) == rooms.end()) {
340 rooms.push_back(room);
341 }
342 }
343 }
344 input.pause_map_rooms.push_back(std::move(rooms));
345 }
346 }
347 }
348
349 if (options.project != nullptr) {
351 }
352 if (options.manifest != nullptr) {
354 }
355 if (options.stop_token.stop_requested()) {
356 return absl::CancelledError("Room census cancelled");
357 }
358 return input;
359}
360
361std::string FormatRoomBlockSummary(const std::vector<int>& rooms_in) {
362 if (rooms_in.empty()) {
363 return "";
364 }
365 std::vector<int> rooms = rooms_in;
366 std::sort(rooms.begin(), rooms.end());
367 rooms.erase(std::unique(rooms.begin(), rooms.end()), rooms.end());
368
369 std::map<int, int> per_row;
370 for (int room : rooms) {
371 ++per_row[room >> 4];
372 }
373 int main_row = rooms.front() >> 4;
374 for (const auto& [row, count] : per_row) {
375 if (count > per_row[main_row]) {
376 main_row = row;
377 }
378 }
379 auto format_runs = [](const std::vector<int>& ids) {
380 std::vector<std::string> parts;
381 for (size_t i = 0; i < ids.size();) {
382 size_t j = i;
383 while (j + 1 < ids.size() && ids[j + 1] == ids[j] + 1 &&
384 (ids[j + 1] >> 4) == (ids[i] >> 4)) {
385 ++j;
386 }
387 parts.push_back(
388 j > i ? absl::StrFormat("%s-%s", RoomHex(ids[i]), RoomHex(ids[j]))
389 : RoomHex(ids[i]));
390 i = j + 1;
391 }
392 return absl::StrJoin(parts, "/");
393 };
394 std::vector<int> main_ids;
395 std::vector<int> other_ids;
396 for (int room : rooms) {
397 ((room >> 4) == main_row ? main_ids : other_ids).push_back(room);
398 }
399 std::string text =
400 absl::StrFormat("Row %X: %s", main_row, format_runs(main_ids));
401 if (!other_ids.empty()) {
402 text += " + " + format_runs(other_ids);
403 }
404 text += absl::StrFormat(", %d room%s", static_cast<int>(rooms.size()),
405 rooms.size() == 1 ? "" : "s");
406 return text;
407}
408
410 RoomCensus census;
411 census.rooms.resize(kRooms);
415
416 std::vector<RoomCensusRoomFacts> facts = input.rooms;
417 facts.resize(kRooms);
418 for (int i = 0; i < kRooms; ++i) {
419 facts[i].room_id = i;
420 }
421
422 // ---- Edges -------------------------------------------------------------
423 // Shared with the z3ed graph commands (room_links.h). Links whose
424 // destination is not a room (a header byte past 0x127) are dropped here;
425 // the lookup never indexes `facts` out of range.
426 const RoomLinkFactsLookup lookup =
427 [&facts](int room) -> const RoomLinkFacts* {
428 return IsDungeonRoomId(room) ? &facts[room] : nullptr;
429 };
430 std::vector<std::vector<RoomLink>> out_edges(kRooms);
431 for (int room = 0; room < kRooms; ++room) {
432 for (auto& link : CollectRoomLinks(room, lookup, input.warp_tag_ids)) {
433 if (link.to >= 0) {
434 out_edges[room].push_back(std::move(link));
435 }
436 }
437 }
438
439 // Inbound references.
440 for (int room = 0; room < kRooms; ++room) {
441 for (const auto& edge : out_edges[room]) {
442 if (edge.to == room) {
443 continue; // A room does not reach itself.
444 }
445 RoomReference ref;
446 ref.kind = edge.kind;
447 ref.strong = edge.strong;
448 ref.from_room = edge.from;
449 ref.detail = edge.detail;
450 census.rooms[edge.to].references.push_back(std::move(ref));
451 }
452 }
453 std::vector<int> roots;
454 for (const auto& entrance : input.entrances) {
455 if (entrance.room_id < 0 || entrance.room_id >= kRooms) {
456 continue;
457 }
458 RoomReference ref;
459 ref.kind = entrance.kind;
460 ref.entrance_id = entrance.entrance_id;
461 ref.strong = entrance.kind != RoomReferenceKind::kUnplacedEntrance;
462 switch (entrance.kind) {
464 ref.detail = absl::StrFormat("spawn point %d", entrance.entrance_id);
465 break;
467 ref.detail = absl::StrFormat("overworld hole -> entrance 0x%02X (%s)",
468 entrance.entrance_id, entrance.placement);
469 break;
471 ref.detail =
472 absl::StrFormat("entrance 0x%02X (not placed on the overworld)",
473 entrance.entrance_id);
474 break;
475 default:
476 ref.detail = absl::StrFormat("entrance 0x%02X (%s)",
477 entrance.entrance_id, entrance.placement);
478 break;
479 }
480 if (ref.strong) {
481 roots.push_back(entrance.room_id);
482 }
483 census.rooms[entrance.room_id].references.push_back(std::move(ref));
484 }
485
486 // ---- Reachability ------------------------------------------------------
487 auto is_empty = [&](int room) {
488 return facts[room].object_count == 0 ||
489 (facts[room].object_count <= 1 && facts[room].sprite_count <= 1);
490 };
491 std::vector<bool> reached(kRooms, false);
492 std::deque<int> queue;
493 for (int root : roots) {
494 if (!reached[root]) {
495 reached[root] = true;
496 queue.push_back(root);
497 }
498 }
499 auto drain = [&]() {
500 while (!queue.empty()) {
501 const int room = queue.front();
502 queue.pop_front();
503 for (const auto& edge : out_edges[room]) {
504 if (edge.strong && !reached[edge.to]) {
505 reached[edge.to] = true;
506 queue.push_back(edge.to);
507 }
508 }
509 }
510 };
511 drain();
512 // Pause maps: big halls and open floors change rooms without a door
513 // object. A non-empty room on the map of a reached dungeon is in use,
514 // with or without project ownership, so reachability depends only on the
515 // ROM (and the project's warp tags). A stale grid entry (Oracle's bank $0A
516 // grids still list 0x01 under dungeon ID 0x02) keeps the room in use until
517 // the grid is edited: the pause map still draws it.
518 std::vector<int> map_dungeon(kRooms, -1);
519 std::vector<bool> map_ref_added(kRooms, false);
520 for (bool changed = true; changed;) {
521 changed = false;
522 for (size_t map = 0; map < input.pause_map_rooms.size(); ++map) {
523 const auto& rooms = input.pause_map_rooms[map];
524 const bool active = std::any_of(
525 rooms.begin(), rooms.end(),
526 [&](int room) { return IsDungeonRoomId(room) && reached[room]; });
527 for (int room : rooms) {
528 if (room < 0 || room >= kRooms) {
529 continue;
530 }
531 if (map_dungeon[room] < 0) {
532 map_dungeon[room] = static_cast<int>(map) * 2;
533 }
534 const bool strong = active && !is_empty(room);
535 if (!map_ref_added[room] || (strong && !reached[room])) {
536 RoomReference ref;
538 ref.strong = strong;
539 ref.detail =
540 absl::StrFormat("on the pause map of dungeon ID 0x%02X%s",
541 static_cast<int>(map) * 2,
542 !active ? " (no room of that map is reached)"
543 : strong ? ""
544 : " (room is empty)");
545 if (!map_ref_added[room]) {
546 census.rooms[room].references.push_back(ref);
547 map_ref_added[room] = true;
548 } else {
549 for (auto& existing : census.rooms[room].references) {
550 if (existing.kind == RoomReferenceKind::kDungeonMap) {
551 existing = ref;
552 break;
553 }
554 }
555 }
556 }
557 if (strong && !reached[room]) {
558 reached[room] = true;
559 queue.push_back(room);
560 changed = true;
561 }
562 }
563 }
564 drain();
565 }
566
567 // ---- Owners ------------------------------------------------------------
568 std::vector<int> owner_of(kRooms, -1);
569 std::vector<bool> project_listed(kRooms, false);
570 census.owners_from_project = !input.project_owners.empty();
571 for (const auto& group : input.project_owners) {
572 RoomCensusOwner owner;
573 owner.id = group.id;
574 owner.name = group.name;
575 owner.from_project = true;
576 const int index = static_cast<int>(census.owners.size());
577 census.owners.push_back(owner);
578 for (int room : group.rooms) {
579 if (room >= 0 && room < kRooms && owner_of[room] < 0) {
580 owner_of[room] = index;
581 project_listed[room] = true;
582 }
583 }
584 }
585
586 // Vanilla dungeon names only fit a ROM that still looks vanilla; a hack
587 // reuses the IDs for its own dungeons.
588 int authored_rooms = 0;
589 int vanilla_rooms = 0;
590 for (const auto& f : facts) {
591 if (f.object_count > 0) {
592 ++authored_rooms;
593 vanilla_rooms += f.vanilla_similarity >= kReclaimableVanillaSimilarity;
594 }
595 }
596 const bool vanilla_names = input.has_vanilla_baseline && authored_rooms > 0 &&
597 vanilla_rooms * 5 >= authored_rooms * 4;
598 census.vanilla_owner_names = vanilla_names;
599
600 std::map<int, int> derived_owner; // dungeon id -> owner index
601 auto derived_owner_index = [&](int dungeon_id) {
602 auto it = derived_owner.find(dungeon_id);
603 if (it != derived_owner.end()) {
604 return it->second;
605 }
606 RoomCensusOwner owner;
607 owner.dungeon_id = dungeon_id;
608 if (dungeon_id == kRoomCensusInteriorDungeonId) {
609 owner.id = "INT";
610 owner.name = "Interiors";
611 owner.interior = true;
612 } else {
613 owner.id = absl::StrFormat("ID%02X", dungeon_id);
614 const std::string vanilla = VanillaDungeonName(dungeon_id);
615 if (census.owners_from_project) {
616 owner.name =
617 absl::StrFormat("Dungeon ID 0x%02X (not in project)", dungeon_id);
618 } else if (!vanilla_names || vanilla.empty() || (dungeon_id & 1) != 0) {
619 owner.name = absl::StrFormat("Dungeon ID 0x%02X", dungeon_id);
620 } else {
621 owner.name = vanilla;
622 }
623 }
624 const int index = static_cast<int>(census.owners.size());
625 census.owners.push_back(owner);
626 derived_owner[dungeon_id] = index;
627 return index;
628 };
629
630 // Dungeon entrances first (by dungeon ID), interiors last.
631 std::vector<const RoomCensusEntranceFact*> ordered;
632 for (const auto& entrance : input.entrances) {
633 if (entrance.kind != RoomReferenceKind::kUnplacedEntrance &&
634 entrance.room_id >= 0 && entrance.room_id < kRooms) {
635 ordered.push_back(&entrance);
636 }
637 }
638 std::stable_sort(
639 ordered.begin(), ordered.end(), [](const auto* a, const auto* b) {
640 const bool a_int = a->dungeon_id == kRoomCensusInteriorDungeonId;
641 const bool b_int = b->dungeon_id == kRoomCensusInteriorDungeonId;
642 if (a_int != b_int) {
643 return !a_int;
644 }
645 if (a->dungeon_id != b->dungeon_id) {
646 return a->dungeon_id < b->dungeon_id;
647 }
648 return a->entrance_id < b->entrance_id;
649 });
650 auto flood = [&](bool owner_edges_only) {
651 for (const auto* entrance : ordered) {
652 const int start = entrance->room_id;
653 if (project_listed[start]) {
654 continue;
655 }
656 int owner = owner_of[start];
657 if (owner < 0) {
658 owner = derived_owner_index(entrance->dungeon_id);
659 owner_of[start] = owner;
660 }
661 std::deque<int> q{start};
662 std::vector<bool> seen(kRooms, false);
663 seen[start] = true;
664 while (!q.empty()) {
665 const int room = q.front();
666 q.pop_front();
667 for (const auto& edge : out_edges[room]) {
668 if (!edge.strong || (owner_edges_only && !edge.owner_ok) ||
669 seen[edge.to] || project_listed[edge.to]) {
670 continue;
671 }
672 seen[edge.to] = true;
673 if (owner_of[edge.to] < 0) {
674 owner_of[edge.to] = owner;
675 } else if (owner_of[edge.to] != owner) {
676 continue; // Another area owns it; do not walk through.
677 }
678 q.push_back(edge.to);
679 }
680 }
681 }
682 };
683 flood(/*owner_edges_only=*/true);
684 // Rooms on a dungeon's pause map belong to that dungeon.
685 for (int room = 0; room < kRooms; ++room) {
686 if (owner_of[room] < 0 && reached[room] && map_dungeon[room] >= 0) {
687 owner_of[room] = derived_owner_index(map_dungeon[room]);
688 }
689 }
690 flood(/*owner_edges_only=*/false);
691
692 // ---- Classify ----------------------------------------------------------
693 for (int room = 0; room < kRooms; ++room) {
694 const auto& f = facts[room];
695 RoomCensusEntry& entry = census.rooms[room];
696 entry.room_id = room;
697 entry.reached = reached[room];
698 entry.owner_index = owner_of[room];
699 entry.interior =
700 entry.owner_index >= 0 && census.owners[entry.owner_index].interior;
701 entry.object_count = f.object_count;
702 entry.sprite_count = f.sprite_count;
703 entry.vanilla_similarity = f.vanilla_similarity;
704 entry.empty =
705 f.object_count == 0 || (f.object_count <= 1 && f.sprite_count <= 1);
706 const std::string content =
707 absl::StrFormat("%d object%s, %d sprite%s", f.object_count,
708 f.object_count == 1 ? "" : "s", f.sprite_count,
709 f.sprite_count == 1 ? "" : "s");
710 const int vanilla_percent =
711 static_cast<int>(f.vanilla_similarity * 100.0f + 0.5f);
712
713 // Several overworld slots can place the same entrance; list it once.
714 std::vector<RoomReference> unique_refs;
715 for (auto& ref : entry.references) {
716 const bool duplicate = std::any_of(
717 unique_refs.begin(), unique_refs.end(), [&](const auto& seen) {
718 return seen.kind == ref.kind && seen.detail == ref.detail;
719 });
720 if (!duplicate) {
721 unique_refs.push_back(std::move(ref));
722 }
723 }
724 entry.references = std::move(unique_refs);
725 std::vector<std::string> strong_refs;
726 std::vector<std::string> weak_refs;
727 for (const auto& ref : entry.references) {
728 (ref.strong ? strong_refs : weak_refs).push_back(ref.detail);
729 }
730 auto join_capped = [](const std::vector<std::string>& items) {
731 constexpr size_t kMaxListed = 4;
732 std::vector<std::string> shown(
733 items.begin(), items.begin() + std::min(items.size(), kMaxListed));
734 std::string text = absl::StrJoin(shown, "; ");
735 if (items.size() > kMaxListed) {
736 text += absl::StrFormat("; +%d more",
737 static_cast<int>(items.size() - kMaxListed));
738 }
739 return text;
740 };
741
742 if (entry.reached) {
743 entry.status = RoomCensusStatus::kInUse;
744 entry.reasons.push_back(strong_refs.empty()
745 ? "reached from an entrance"
746 : "reached via " + join_capped(strong_refs));
747 } else if (project_listed[room]) {
748 entry.status = RoomCensusStatus::kInUse;
749 entry.orphan = true;
750 entry.reasons.push_back(absl::StrFormat(
751 "listed in project dungeon %s, but no entrance, stair, hole, warp "
752 "or door path reaches it",
753 census.owners[owner_of[room]].id));
754 } else if (entry.empty) {
755 entry.status = RoomCensusStatus::kFree;
756 entry.reasons.push_back(
757 "no entrance, stair, hole, warp or door path reaches it");
758 entry.reasons.push_back("empty: " + content);
759 } else if (f.vanilla_similarity >= kReclaimableVanillaSimilarity) {
760 entry.status = RoomCensusStatus::kReclaimable;
761 entry.reasons.push_back(
762 "no entrance, stair, hole, warp or door path reaches it");
763 entry.reasons.push_back(absl::StrFormat(
764 "vanilla leftover: %d%% of its objects match vanilla (%s)",
765 vanilla_percent, content));
766 } else if (f.vanilla_similarity >= kPartlyEditedVanillaSimilarity) {
767 entry.status = RoomCensusStatus::kReclaimable;
768 entry.reasons.push_back(
769 "no entrance, stair, hole, warp or door path reaches it");
770 entry.reasons.push_back(absl::StrFormat(
771 "partly edited vanilla leftover: %d%% of its objects match vanilla "
772 "(%s); check the edits before reclaiming",
773 vanilla_percent, content));
774 } else {
775 entry.status = RoomCensusStatus::kInUse;
776 entry.orphan = true;
777 entry.reasons.push_back(
778 "orphan: authored content but no entrance, stair, hole, warp or "
779 "door path reaches it; owner decision needed");
780 entry.reasons.push_back(
781 f.vanilla_similarity < 0.0f
782 ? content
783 : absl::StrFormat("%s, %d%% vanilla", content, vanilla_percent));
784 }
785 if (!entry.reached && !weak_refs.empty()) {
786 entry.reasons.push_back("caveat: " + join_capped(weak_refs));
787 }
788 if (entry.status != RoomCensusStatus::kInUse && f.in_pit_damage_table) {
789 entry.reasons.push_back("caveat: listed in RoomsWithPitDamage");
790 }
791
792 switch (entry.status) {
794 ++census.free_count;
795 break;
797 ++census.reclaimable_count;
798 break;
800 ++census.in_use_count;
801 break;
802 }
803 census.orphan_count += entry.orphan ? 1 : 0;
804 census.interior_count += entry.interior ? 1 : 0;
805 if (entry.owner_index >= 0) {
806 ++census.owners[entry.owner_index].room_count;
807 }
808 }
809
810 // ---- Free blocks -------------------------------------------------------
811 // Contiguous runs ("cores") are 4-connected free rooms on one $A1 page. A
812 // core of at least kFreeBlockMinCore rooms also takes in smaller free cores
813 // within kFreeBlockMergeDistance cells (not transitively), so a block reads
814 // like "Row 9: 0x93-0x96 + 0xA0/0xA6/0xB0" while scattered single rooms
815 // stay separate.
816 std::vector<int> free_rooms;
817 for (const auto& entry : census.rooms) {
818 if (entry.status == RoomCensusStatus::kFree) {
819 free_rooms.push_back(entry.room_id);
820 }
821 }
822 auto grid_distance = [](int a, int b) {
823 if ((a >> 8) != (b >> 8)) {
824 return 1 << 20;
825 }
826 const int dr = std::abs(((a & 0xFF) >> 4) - ((b & 0xFF) >> 4));
827 const int dc = std::abs((a & 0x0F) - (b & 0x0F));
828 return std::max(dr, dc);
829 };
830 auto adjacent = [](int a, int b) {
831 if ((a >> 8) != (b >> 8)) {
832 return false;
833 }
834 const int dr = std::abs(((a & 0xFF) >> 4) - ((b & 0xFF) >> 4));
835 const int dc = std::abs((a & 0x0F) - (b & 0x0F));
836 return dr + dc == 1;
837 };
838 std::vector<std::vector<int>> cores;
839 std::vector<bool> assigned(free_rooms.size(), false);
840 for (size_t i = 0; i < free_rooms.size(); ++i) {
841 if (assigned[i]) {
842 continue;
843 }
844 std::vector<int> core;
845 std::deque<size_t> pending{i};
846 assigned[i] = true;
847 while (!pending.empty()) {
848 const size_t at = pending.front();
849 pending.pop_front();
850 core.push_back(free_rooms[at]);
851 for (size_t j = 0; j < free_rooms.size(); ++j) {
852 if (!assigned[j] && adjacent(free_rooms[at], free_rooms[j])) {
853 assigned[j] = true;
854 pending.push_back(j);
855 }
856 }
857 }
858 std::sort(core.begin(), core.end());
859 cores.push_back(std::move(core));
860 }
861 std::stable_sort(cores.begin(), cores.end(),
862 [](const auto& a, const auto& b) {
863 if (a.size() != b.size()) {
864 return a.size() > b.size();
865 }
866 return a.front() < b.front();
867 });
868 std::vector<bool> absorbed(cores.size(), false);
869 for (size_t i = 0; i < cores.size(); ++i) {
870 if (absorbed[i]) {
871 continue;
872 }
873 absorbed[i] = true;
874 RoomCensusCluster cluster;
875 cluster.core_rooms = cores[i];
876 cluster.rooms = cores[i];
877 if (static_cast<int>(cores[i].size()) >= kFreeBlockMinCore) {
878 for (size_t j = i + 1; j < cores.size(); ++j) {
879 if (absorbed[j] || cores[j].size() >= cores[i].size()) {
880 continue;
881 }
882 bool near = false;
883 for (int a : cores[i]) {
884 for (int b : cores[j]) {
885 near |= grid_distance(a, b) <= kFreeBlockMergeDistance;
886 }
887 }
888 if (near) {
889 absorbed[j] = true;
890 cluster.rooms.insert(cluster.rooms.end(), cores[j].begin(),
891 cores[j].end());
892 }
893 }
894 }
895 std::sort(cluster.rooms.begin(), cluster.rooms.end());
896 cluster.summary = FormatRoomBlockSummary(cluster.rooms);
897 census.free_clusters.push_back(std::move(cluster));
898 }
899 std::stable_sort(census.free_clusters.begin(), census.free_clusters.end(),
900 [](const auto& a, const auto& b) {
901 if (a.rooms.size() != b.rooms.size()) {
902 return a.rooms.size() > b.rooms.size();
903 }
904 return a.rooms.front() < b.rooms.front();
905 });
906 return census;
907}
908
909absl::StatusOr<RoomCensus> ComputeRoomCensus(Rom* rom,
910 const RoomCensusOptions& options) {
911 auto input = CollectRoomCensusInput(rom, options);
912 if (!input.ok()) {
913 return input.status();
914 }
915 if (options.stop_token.stop_requested()) {
916 return absl::CancelledError("Room census cancelled");
917 }
918 auto census = BuildRoomCensus(*input);
919 if (options.stop_token.stop_requested()) {
920 return absl::CancelledError("Room census cancelled");
921 }
922 return census;
923}
924
925} // namespace yaze::zelda3
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
auto filename() const
Definition rom.h:207
const auto & vector() const
Definition rom.h:205
bool is_loaded() const
Definition rom.h:187
Loads and queries the hack manifest JSON for yaze-ASM integration.
const std::vector< RoomTagEntry > & room_tags() const
Get all room tags.
bool stop_requested() const noexcept
Definition stop_token.h:21
static absl::StatusOr< DungeonSpawnPoint > Load(const Rom &rom, int spawn_id)
static absl::Status LoadFromRom(Rom *rom, PitDamageTable *out)
const std::vector< chest_data > & GetChests() const
Definition room.h:323
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void LoadSprites()
Definition room.cc:2886
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::vector< uint16_t > FingerprintRoom(const Room &room)
bool IsEncodedStreamObject(const RoomObject &object)
Zelda 3 specific classes and functions.
std::vector< uint16_t > BuiltinVanillaRoomFingerprint(int room_id)
absl::StatusOr< RoomCensusInput > CollectRoomCensusInput(Rom *rom, const RoomCensusOptions &options)
absl::StatusOr< std::vector< OverworldEntrance > > LoadEntrances(Rom *rom)
std::vector< RoomCensusOwnerGroup > RoomCensusOwnersFromProject(const core::ProjectRegistry &registry)
std::set< uint8_t > RoomCensusWarpTagsFromManifest(const core::HackManifest &manifest)
std::optional< DungeonEntranceTarget > ReadDungeonEntranceTarget(const Rom &rom, int entrance_id)
absl::StatusOr< RoomCensus > ComputeRoomCensus(Rom *rom, const RoomCensusOptions &options)
std::string FormatRoomBlockSummary(const std::vector< int > &rooms_in)
bool IsDungeonRoomId(int room_id)
constexpr int kRoomCensusInteriorDungeonId
Definition room_census.h:61
const char * RoomCensusStatusName(RoomCensusStatus status)
absl::StatusOr< std::vector< std::vector< uint16_t > > > ComputeRoomFingerprints(Rom *rom, util::StopToken stop_token)
std::vector< RoomLink > CollectRoomLinks(int room_id, const RoomLinkFactsLookup &lookup, const std::set< uint8_t > &warp_tag_ids)
absl::StatusOr< RoomLinkFacts > CollectRoomLinkFacts(Rom *rom, const Room &room, const PitDamageTable *pit_table, const CustomObjectRenderSnapshot *custom_objects, util::StopToken stop_token)
constexpr float kReclaimableVanillaSimilarity
Definition room_census.h:56
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
constexpr int kDungeonEntranceCount
Definition room_links.h:53
RoomCensus BuildRoomCensus(const RoomCensusInput &input)
constexpr int kNumDungeonSpawnPoints
absl::StatusOr< std::vector< OverworldEntrance > > LoadHoles(Rom *rom)
std::array< std::vector< std::array< std::string, kNumRooms > >, kNumDungeons > DungeonMapLabels
Definition dungeon_map.h:72
absl::StatusOr< std::vector< DungeonMap > > LoadDungeonMaps(Rom &rom, DungeonMapLabels &dungeon_map_labels)
Load the dungeon maps from the ROM.
std::function< const RoomLinkFacts *(int room_id)> RoomLinkFactsLookup
Definition room_links.h:147
float RoomFingerprintSimilarity(const std::vector< uint16_t > &a, const std::vector< uint16_t > &b)
constexpr float kPartlyEditedVanillaSimilarity
Definition room_census.h:60
constexpr int kRoomCensusRoomCount
Definition room_census.h:55
absl::StatusOr< EntranceLayout > ResolveEntranceLayout(std::span< const uint8_t > data)
uint16_t HashRoomCensusObject(int id, int x, int y, int size)
Project-level registry data loaded from the Oracle planning outputs.
std::vector< DungeonEntry > dungeons
std::vector< RoomCensusEntranceFact > entrances
std::vector< std::vector< int > > pause_map_rooms
std::vector< RoomCensusOwnerGroup > project_owners
std::set< uint8_t > warp_tag_ids
std::vector< RoomCensusRoomFacts > rooms
std::function< void(int room_id, const Room &room) room_visitor)
const core::RomAddressOverrides * address_overrides
const core::ProjectRegistry * project
const core::HackManifest * manifest
const CustomObjectRenderSnapshot * custom_objects
std::vector< RoomCensusOwner > owners
std::string vanilla_baseline_source
std::vector< RoomCensusEntry > rooms
RoomReferenceKind kind
Definition room_census.h:66