yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_sheet_inventory.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <regex>
6#include <sstream>
7#include <utility>
8
9#include "absl/strings/str_format.h"
11#include "nlohmann/json.hpp"
12#include "rom/snes.h"
13#include "util/rom_hash.h"
15#include "zelda3/game_data.h"
18
19namespace yaze::zelda3 {
20namespace {
21
22constexpr uint32_t kSpritesetTable = 0x5B57;
23constexpr int kSpritesetCount = 144;
24constexpr uint32_t kRoomBlocksetTable = 0x5D97;
25constexpr int kRoomBlocksetCount = 82;
26constexpr uint32_t kMainBlocksetPointer = 0x6237;
27constexpr int kMainBlocksetCount = 37;
28constexpr size_t kTileBytes3bpp = 24;
29constexpr size_t kMeasureLimit = 0x8000;
30
31template <size_t N>
32bool Contains(const std::array<uint8_t, N>& values, int value) {
33 return std::find(values.begin(), values.end(), value) != values.end();
34}
35
36// Splits CSV text into rows of fields (RFC 4180 quoting).
37std::vector<std::vector<std::string>> ParseCsv(const std::string& text) {
38 std::vector<std::vector<std::string>> rows;
39 std::vector<std::string> row;
40 std::string field;
41 bool quoted = false;
42 for (size_t i = 0; i < text.size(); ++i) {
43 const char c = text[i];
44 if (quoted) {
45 if (c == '"' && i + 1 < text.size() && text[i + 1] == '"') {
46 field.push_back('"');
47 ++i;
48 } else if (c == '"') {
49 quoted = false;
50 } else {
51 field.push_back(c);
52 }
53 } else if (c == '"') {
54 quoted = true;
55 } else if (c == ',') {
56 row.push_back(std::move(field));
57 field.clear();
58 } else if (c == '\n' || c == '\r') {
59 if (c == '\r' && i + 1 < text.size() && text[i + 1] == '\n') {
60 ++i;
61 }
62 row.push_back(std::move(field));
63 field.clear();
64 rows.push_back(std::move(row));
65 row.clear();
66 } else {
67 field.push_back(c);
68 }
69 }
70 if (!field.empty() || !row.empty()) {
71 row.push_back(std::move(field));
72 rows.push_back(std::move(row));
73 }
74 return rows;
75}
76
77} // namespace
78
79std::vector<int> GfxSheetInventoryEntry::FreeBlocks() const {
80 std::vector<int> free;
82 return free;
83 }
84 for (int block : empty_16x16_blocks) {
85 if (std::find(reserved_blocks.begin(), reserved_blocks.end(), block) ==
86 reserved_blocks.end()) {
87 free.push_back(block);
88 }
89 }
90 return free;
91}
92
93absl::StatusOr<GfxSheetInventory> BuildGfxSheetInventory(
94 const Rom& rom, const std::map<int, OverworldAreaGfxInfo>& areas,
95 const std::map<int, RoomGfxInfo>& rooms,
96 const GfxSheetInventoryOptions& options) {
97 if (!rom.is_loaded()) {
98 return absl::FailedPreconditionError("ROM not loaded");
99 }
100 const auto& bytes = rom.vector();
101 GfxSheetInventory inventory;
102 inventory.overworld_areas = areas;
103 inventory.rooms = rooms;
104
105 // Sheets.
106 inventory.sheets.resize(kGfxSheetCount);
107 for (uint16_t gfx = 0; gfx < kGfxSheetCount; ++gfx) {
108 auto& entry = inventory.sheets[gfx];
109 entry.gfx = gfx;
110 entry.kind = GetGfxSheetStorageKind(gfx);
111 if (gfx >= kSpriteSheetBase &&
113 entry.sprite_value = static_cast<uint8_t>(gfx - kSpriteSheetBase);
114 }
115 entry.reserved = options.reserved_sheets.count(gfx) != 0;
116 entry.flagged = options.flagged_sheets.count(gfx) != 0;
117 if (auto it = options.reserved_blocks.find(gfx);
118 it != options.reserved_blocks.end()) {
119 entry.reserved_blocks = it->second;
120 }
121 entry.engine_loaded = entry.kind != GfxSheetStorageKind::kCompressed3bpp;
122
123 auto pc = ReadGfxSheetPc(rom, gfx);
124 if (!pc.ok()) {
125 entry.error = std::string(pc.status().message());
126 continue;
127 }
128 entry.pc = *pc;
129
130 std::vector<uint8_t> data;
131 if (entry.kind == GfxSheetStorageKind::kRaw3bpp) {
132 if (entry.pc + kGfxSheet3bppBytes > bytes.size()) {
133 entry.error = "raw sheet runs past the ROM end";
134 continue;
135 }
136 data.assign(bytes.begin() + entry.pc,
137 bytes.begin() + entry.pc + kGfxSheet3bppBytes);
138 entry.stored_bytes = kGfxSheet3bppBytes;
139 } else if (entry.kind == GfxSheetStorageKind::kCompressed3bpp) {
140 auto decoded = gfx::lc_lz2::DecompressExact(bytes.data(), bytes.size(),
141 entry.pc, kMeasureLimit,
142 /*big_endian_copy=*/false);
143 if (!decoded.ok()) {
144 entry.error = std::string(decoded.status().message());
145 continue;
146 }
147 entry.stored_bytes = decoded->compressed_size;
148 data = std::move(decoded->data);
149 data.resize(kGfxSheet3bppBytes, 0);
150 } else {
151 continue; // 2bpp: engine-loaded, no block statistics.
152 }
153
154 entry.has_block_stats = true;
155 for (int tile = 0; tile < 64; ++tile) {
156 const auto begin = data.begin() + tile * kTileBytes3bpp;
157 if (std::all_of(begin, begin + kTileBytes3bpp,
158 [](uint8_t b) { return b == 0; })) {
159 ++entry.empty_8x8;
160 }
161 }
162 for (int block = 0; block < kGfxSheetBlockCount; ++block) {
163 if (IsGfxSheetBlockEmpty(data, block, 3)) {
164 entry.empty_16x16_blocks.push_back(block);
165 }
166 }
167 entry.all_empty = entry.empty_8x8 == 64;
168 entry.sha1 = util::ComputeSha1Hex(data.data(), data.size()).substr(0, 12);
169 }
170
171 // Shared pointers and identical contents.
172 std::map<uint32_t, std::vector<uint16_t>> by_pc;
173 std::map<std::string, std::vector<uint16_t>> by_sha;
174 for (const auto& entry : inventory.sheets) {
175 if (entry.error.empty() || entry.pc != 0) {
176 by_pc[entry.pc].push_back(entry.gfx);
177 }
178 if (!entry.sha1.empty() && !entry.all_empty) {
179 by_sha[entry.sha1].push_back(entry.gfx);
180 }
181 }
182 for (auto& entry : inventory.sheets) {
183 for (uint16_t other : by_pc[entry.pc]) {
184 if (other != entry.gfx) {
185 entry.shares_pointer_with.push_back(other);
186 }
187 }
188 if (!entry.sha1.empty() && !entry.all_empty) {
189 for (uint16_t other : by_sha[entry.sha1]) {
190 if (other != entry.gfx) {
191 entry.same_content_as.push_back(other);
192 }
193 }
194 }
195 }
196
197 // Group tables.
198 auto read = [&bytes](size_t pc) -> uint8_t {
199 return pc < bytes.size() ? bytes[pc] : 0;
200 };
201 for (int n = 0; n < kSpritesetCount; ++n) {
202 std::array<uint8_t, 4> slots{};
203 for (int j = 0; j < 4; ++j) {
204 slots[j] = read(kSpritesetTable + n * 4 + j);
205 }
206 inventory.spritesets.push_back(slots);
207 }
208 for (int n = 0; n < kRoomBlocksetCount; ++n) {
209 std::array<uint8_t, 4> slots{};
210 for (int j = 0; j < 4; ++j) {
211 slots[j] = read(kRoomBlocksetTable + n * 4 + j);
212 }
213 inventory.room_blocksets.push_back(slots);
214 }
215 const uint32_t main_pc = SnesToPc(read(kMainBlocksetPointer) |
216 (read(kMainBlocksetPointer + 1) << 8));
217 for (int n = 0; n < kMainBlocksetCount; ++n) {
218 std::array<uint8_t, 8> slots{};
219 for (int j = 0; j < 8; ++j) {
220 slots[j] = read(main_pc + n * 8 + j);
221 }
222 inventory.main_blocksets.push_back(slots);
223 }
224
225 // Usage per sheet.
226 for (auto& entry : inventory.sheets) {
227 const int gfx = entry.gfx;
228 auto& use = entry.usage;
229 for (int n = 0; n < kMainBlocksetCount; ++n) {
230 if (Contains(inventory.main_blocksets[n], gfx)) {
231 use.main_blocksets.push_back(n);
232 }
233 }
234 for (int n = 0; n < kRoomBlocksetCount; ++n) {
235 if (Contains(inventory.room_blocksets[n], gfx)) {
236 use.room_blocksets.push_back(n);
237 }
238 }
239 for (const auto& [area, info] : areas) {
240 if (Contains(info.static_graphics, gfx)) {
241 use.ow_areas_static.push_back(area);
242 }
243 }
244 if (entry.sprite_value.has_value()) {
245 const int value = *entry.sprite_value;
246 for (int n = 0; n < kSpritesetCount; ++n) {
247 if (Contains(inventory.spritesets[n], value)) {
248 use.spritesets.push_back(n);
249 }
250 }
251 auto uses_set = [&use](int set) {
252 return std::binary_search(use.spritesets.begin(), use.spritesets.end(),
253 set);
254 };
255 for (const auto& [area, info] : areas) {
256 if (std::any_of(info.sprite_graphics.begin(),
257 info.sprite_graphics.end(), uses_set)) {
258 use.ow_areas_sprite.push_back(area);
259 }
260 }
261 for (const auto& [room, info] : rooms) {
262 if (uses_set(info.spriteset + kDungeonSpritesetBase)) {
263 use.rooms_sprite.push_back(room);
264 }
265 }
266 if (auto it = options.sprite_value_labels.find(value);
267 it != options.sprite_value_labels.end()) {
268 entry.labels.assign(it->second.begin(), it->second.end());
269 }
270 }
271 entry.unreferenced = !use.Any() && !entry.engine_loaded;
272 }
273 return inventory;
274}
275
276std::map<int, OverworldAreaGfxInfo> CollectOverworldAreaGfx(
277 Rom& rom, GameData* game_data) {
278 std::map<int, OverworldAreaGfxInfo> areas;
279 if (!rom.is_loaded()) {
280 return areas;
281 }
282 for (int index = 0; index < kNumOverworldAreas; ++index) {
283 OverworldMap map(index, &rom, game_data);
284 map.set_game_state(0);
285 map.LoadAreaGraphics();
287 info.world = index < 0x40 ? 0 : (index < 0x80 ? 1 : 2);
288 info.parent = map.parent();
289 info.area_graphics = map.area_graphics();
290 for (int i = 0; i < 3; ++i) {
291 info.sprite_graphics[i] = map.sprite_graphics(i);
292 }
293 for (int i = 0; i < 16; ++i) {
294 info.static_graphics[i] = map.static_graphics(i);
295 }
296 areas[index] = info;
297 }
298 return areas;
299}
300
301std::map<int, RoomGfxInfo> CollectRoomGfx(const Rom& rom) {
302 std::map<int, RoomGfxInfo> rooms;
303 const auto& bytes = rom.vector();
304 auto in_range = [&bytes](int64_t pc, int64_t count) {
305 return pc >= 0 && pc + count <= static_cast<int64_t>(bytes.size());
306 };
307 if (!in_range(kRoomHeaderPointer, 3) ||
308 !in_range(kRoomHeaderPointerBank, 1)) {
309 return rooms;
310 }
311 const uint32_t table = SnesToPc((bytes[kRoomHeaderPointer + 2] << 16) |
312 (bytes[kRoomHeaderPointer + 1] << 8) |
313 bytes[kRoomHeaderPointer]);
314 for (int room = 0; room < kNumDungeonRooms; ++room) {
315 const int64_t slot = static_cast<int64_t>(table) + room * 2;
316 if (!in_range(slot, 2)) {
317 continue;
318 }
319 const uint32_t header = SnesToPc((bytes[kRoomHeaderPointerBank] << 16) |
320 (bytes[slot + 1] << 8) | bytes[slot]);
321 // LoadRoomHeaderFromRom requires 14 readable header bytes.
322 if (!in_range(header, 14)) {
323 continue;
324 }
325 rooms[room] = {bytes[header + 2], bytes[header + 3]};
326 }
327 return rooms;
328}
329
331 int spriteset, const std::map<int, OverworldAreaGfxInfo>& areas,
332 const std::map<int, RoomGfxInfo>& rooms) {
333 GfxGroupUsage usage;
334 for (const auto& [area, info] : areas) {
335 for (int state = 0; state < 3; ++state) {
336 if (info.sprite_graphics[state] == spriteset) {
337 usage.ow_areas_by_state[state].push_back(area);
338 }
339 }
340 }
341 for (const auto& [room, info] : rooms) {
342 if (info.spriteset + kDungeonSpritesetBase == spriteset) {
343 usage.rooms.push_back(room);
344 }
345 }
346 return usage;
347}
348
350 const std::map<int, OverworldAreaGfxInfo>& areas,
351 const std::map<int, RoomGfxInfo>& rooms) {
352 GfxGroupUsage usage;
353 for (const auto& [area, info] : areas) {
354 if (info.area_graphics == roomset) {
355 usage.ow_areas.push_back(area);
356 }
357 }
358 for (const auto& [room, info] : rooms) {
359 if (info.blockset == roomset) {
360 usage.rooms.push_back(room);
361 }
362 }
363 return usage;
364}
365
366absl::StatusOr<RoomsetUsageReport> DescribeRoomsetUsage(
367 Rom& rom, GameData& game_data, int roomset,
368 const std::map<int, RoomGfxInfo>& rooms, const Overworld* live_overworld) {
369 if (!rom.is_loaded()) {
370 return absl::FailedPreconditionError("ROM not loaded");
371 }
372 if (roomset < 0 || roomset >= static_cast<int>(kNumRoomBlocksets)) {
373 return absl::InvalidArgumentError("Invalid roomset index");
374 }
375 if (!game_data.gfx_groups_loaded) {
376 return absl::FailedPreconditionError("Graphics groups are not loaded");
377 }
378 if (live_overworld && live_overworld->rom() != &rom) {
379 return absl::FailedPreconditionError(
380 "Live overworld belongs to a different ROM");
381 }
382
383 RoomsetUsageReport report;
384 report.uses_live_overworld =
385 live_overworld != nullptr && live_overworld->is_loaded();
386 std::map<int, RoomGfxInfo> scanned_rooms;
387 for (const auto& [id, info] : rooms) {
388 if (id >= 0 && id < kNumDungeonRooms) {
389 scanned_rooms.emplace(id, info);
390 }
391 }
392 report.unscanned_rooms = kNumDungeonRooms - scanned_rooms.size();
393
394 std::map<int, OverworldAreaGfxInfo> scanned_areas;
395 std::map<int, OverworldRoomsetSource> sources;
396 // A metadata-only map constructor needs a complete vanilla image or the
397 // expanded metadata tables. Short ROMs can still report room references.
398 const bool can_scan_rom =
399 rom.size() >=
401 ? 0x100000u
402 : 0x200000u);
403 if (report.uses_live_overworld) {
404 for (int id = 0; id < kNumOverworldAreas; ++id) {
405 const auto* map = live_overworld->overworld_map(id);
406 if (!map) {
407 continue;
408 }
410 info.area_graphics = map->area_graphics();
411 scanned_areas.emplace(id, info);
412 sources.emplace(id, map->roomset_source());
413 }
414 } else if (can_scan_rom) {
415 for (int id = 0; id < kNumOverworldAreas; ++id) {
416 OverworldMap map(id, &rom, &game_data);
418 info.area_graphics = map.area_graphics();
419 scanned_areas.emplace(id, info);
420 sources.emplace(id, map.roomset_source());
421 }
422 }
423 report.unscanned_ow_areas = kNumOverworldAreas - scanned_areas.size();
424
425 const GfxGroupUsage raw =
426 FindRoomsetUsage(roomset, scanned_areas, scanned_rooms);
427 report.rooms = raw.rooms;
428 for (int id : raw.ow_areas) {
429 switch (sources.at(id)) {
431 report.ow_areas.push_back(id);
432 break;
434 report.overridden_ow_areas.push_back(id);
435 break;
437 report.independent_ow_areas.push_back(id);
438 break;
439 }
440 }
441 return report;
442}
443
444absl::StatusOr<MainGroupUsageReport> DescribeMainGroupUsage(
445 Rom& rom, GameData& game_data, int group, int slot,
446 const std::map<int, RoomGfxInfo>& rooms,
447 const std::map<int, uint8_t>& explicit_room_main,
448 const Overworld* live_overworld) {
449 if (!rom.is_loaded()) {
450 return absl::FailedPreconditionError("ROM not loaded");
451 }
452 if (group < 0 || group >= static_cast<int>(kNumMainBlocksets) || slot < 0 ||
453 slot >= 8) {
454 return absl::InvalidArgumentError("Invalid main group or slot index");
455 }
456 if (!game_data.gfx_groups_loaded) {
457 return absl::FailedPreconditionError("Graphics groups are not loaded");
458 }
459 if (live_overworld && live_overworld->rom() != &rom) {
460 return absl::FailedPreconditionError(
461 "Live overworld belongs to a different ROM");
462 }
463
465 report.slot7_bottom_half_only = slot == 7;
466 report.uses_live_overworld =
467 live_overworld != nullptr && live_overworld->is_loaded();
468 int scanned_rooms = 0;
469 for (const auto& [id, info] : rooms) {
470 if (id < 0 || id >= kNumDungeonRooms) {
471 continue;
472 }
473 ++scanned_rooms;
474 const auto explicit_it = explicit_room_main.find(id);
475 const uint8_t requested =
476 explicit_it == explicit_room_main.end() ? 0xFF : explicit_it->second;
477 const auto resolved =
478 ResolveRoomMainBlockset(id, info.blockset, requested, game_data);
479 if (resolved.main_blockset != group) {
480 continue;
481 }
482 if (slot >= 3 && slot <= 6 &&
483 info.blockset < game_data.room_blockset_ids.size() &&
484 game_data.room_blockset_ids[info.blockset][slot - 3] != 0) {
485 report.overridden_rooms.push_back(id);
486 continue;
487 }
488 switch (resolved.source) {
490 report.explicit_preview_rooms.push_back(id);
491 break;
493 report.direct_default_rooms.push_back(id);
494 break;
496 report.inferred_default_rooms.push_back(id);
497 break;
500 report.fallback_rooms.push_back(id);
501 break;
502 }
503 }
504 report.unscanned_rooms = kNumDungeonRooms - scanned_rooms;
505
506 const bool can_scan_rom =
507 rom.size() >=
509 ? 0x100000u
510 : 0x200000u);
511 int scanned_ow_areas = 0;
512 auto record_area = [&](int id, const OverworldMap& map) {
513 ++scanned_ow_areas;
514 const auto provenance = map.MainSlotProvenance(slot);
515 if (provenance.main_group_id != group) {
516 return;
517 }
518 switch (provenance.source) {
520 report.ow_areas.push_back(id);
521 break;
524 report.overridden_ow_areas.push_back(id);
525 break;
527 report.independent_ow_areas.push_back(id);
528 break;
529 }
530 };
531 if (report.uses_live_overworld) {
532 for (int id = 0; id < kNumOverworldAreas; ++id) {
533 if (const auto* map = live_overworld->overworld_map(id)) {
534 record_area(id, *map);
535 }
536 }
537 } else if (can_scan_rom) {
538 for (int id = 0; id < kNumOverworldAreas; ++id) {
539 OverworldMap map(id, &rom, &game_data);
540 record_area(id, map);
541 }
542 }
543 report.unscanned_ow_areas = kNumOverworldAreas - scanned_ow_areas;
544 return report;
545}
546
548 switch (kind) {
550 return "3bpp-lz2";
552 return "3bpp-raw";
554 return "2bpp";
555 }
556 return "unknown";
557}
558
559nlohmann::json GfxSheetInventoryToJson(const GfxSheetInventory& inventory) {
560 using nlohmann::json;
561 json out = json::object();
562
563 json sheets = json::array();
564 for (const auto& entry : inventory.sheets) {
565 json sheet = json::object();
566 sheet["gfx"] = entry.gfx;
567 sheet["hex"] = absl::StrFormat("0x%02X", entry.gfx);
568 sheet["pc"] = absl::StrFormat("0x%06X", entry.pc);
569 sheet["kind"] = GfxSheetInventoryKindName(entry.kind);
570 if (entry.sprite_value.has_value()) {
571 sheet["sprite_value"] = absl::StrFormat("0x%02X", *entry.sprite_value);
572 }
573 if (entry.error.empty()) {
574 sheet["stored_bytes"] = entry.stored_bytes.has_value()
575 ? json(*entry.stored_bytes)
576 : json(nullptr);
577 }
578 if (entry.has_block_stats) {
579 sheet["empty_8x8"] = entry.empty_8x8;
580 sheet["empty_16x16"] = entry.empty_16x16_blocks.size();
581 sheet["empty_16x16_blocks"] = entry.empty_16x16_blocks;
582 sheet["all_empty"] = entry.all_empty;
583 sheet["sha1"] = entry.sha1;
584 sheet["free_16x16_blocks"] = entry.FreeBlocks();
585 }
586 if (!entry.error.empty()) {
587 sheet["error"] = entry.error;
588 }
589 if (entry.reserved) {
590 sheet["reserved"] = "reserved by project config";
591 }
592 if (entry.flagged) {
593 sheet["flagged"] = "needs owner approval before use";
594 }
595 if (!entry.reserved_blocks.empty()) {
596 sheet["reserved_blocks"] = entry.reserved_blocks;
597 }
598 if (entry.engine_loaded) {
599 sheet["engine_loaded"] = true;
600 }
601 sheet["shares_pointer_with"] = entry.shares_pointer_with;
602 if (!entry.sha1.empty() && !entry.all_empty) {
603 sheet["same_content_as"] = entry.same_content_as;
604 }
605 json usage = json::object();
606 usage["main_blocksets"] = entry.usage.main_blocksets;
607 usage["room_blocksets"] = entry.usage.room_blocksets;
608 usage["ow_areas_static"] = entry.usage.ow_areas_static;
609 if (entry.sprite_value.has_value()) {
610 usage["spritesets"] = entry.usage.spritesets;
611 usage["ow_areas_sprite"] = entry.usage.ow_areas_sprite;
612 usage["rooms_sprite"] = entry.usage.rooms_sprite;
613 }
614 sheet["usage"] = std::move(usage);
615 if (!entry.labels.empty()) {
616 sheet["labels"] = entry.labels;
617 }
618 sheet["unreferenced"] = entry.unreferenced;
619 sheets.push_back(std::move(sheet));
620 }
621 out["sheets"] = std::move(sheets);
622
623 auto table = [](const auto& rows) {
624 json object = json::object();
625 for (size_t n = 0; n < rows.size(); ++n) {
626 object[std::to_string(n)] =
627 std::vector<int>(rows[n].begin(), rows[n].end());
628 }
629 return object;
630 };
631 out["spritesets"] = table(inventory.spritesets);
632 out["main_blocksets"] = table(inventory.main_blocksets);
633 out["room_blocksets"] = table(inventory.room_blocksets);
634
635 static constexpr const char* kWorlds[] = {"Light", "Dark", "Special"};
636 json areas = json::object();
637 for (const auto& [index, info] : inventory.overworld_areas) {
638 areas[std::to_string(index)] = {
639 {"world", kWorlds[std::clamp(info.world, 0, 2)]},
640 {"parent", absl::StrFormat("0x%02X", info.parent)},
641 {"static_graphics", std::vector<int>(info.static_graphics.begin(),
642 info.static_graphics.end())},
643 {"sprite_graphics", std::vector<int>(info.sprite_graphics.begin(),
644 info.sprite_graphics.end())},
645 {"area_graphics", absl::StrFormat("0x%02X", info.area_graphics)},
646 };
647 }
648 out["overworld_areas"] = std::move(areas);
649
650 json rooms = json::object();
651 for (const auto& [index, info] : inventory.rooms) {
652 rooms[std::to_string(index)] = {{"blockset", info.blockset},
653 {"spriteset", info.spriteset}};
654 }
655 out["rooms"] = std::move(rooms);
656 return out;
657}
658
659std::map<uint8_t, std::set<std::string>> ParseSpritesetLabelCsv(
660 const std::string& csv_text) {
661 static const std::regex kCell(R"(^\s*0x([0-9A-Fa-f]{2})\s+([^\n]+))");
662 std::map<uint8_t, std::set<std::string>> labels;
663 for (const auto& row : ParseCsv(csv_text)) {
664 for (size_t column = 2; column < 6 && column < row.size(); ++column) {
665 std::smatch match;
666 if (!std::regex_search(row[column], match, kCell)) {
667 continue;
668 }
669 std::string name = match[2].str();
670 while (!name.empty() &&
671 std::isspace(static_cast<unsigned char>(name.back()))) {
672 name.pop_back();
673 }
674 const auto value =
675 static_cast<uint8_t>(std::stoi(match[1].str(), nullptr, 16));
676 labels[value].insert(std::move(name));
677 }
678 }
679 return labels;
680}
681
682} // 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
const auto & vector() const
Definition rom.h:205
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
Represents a single Overworld map screen.
auto sprite_graphics(int i) const
auto static_graphics(int i) const
OverworldRoomsetSource roomset_source() const
void set_game_state(int state)
static OverworldVersion GetVersion(const Rom &rom)
Detect ROM version from ASM marker byte.
auto is_loaded() const
Definition overworld.h:803
auto overworld_map(int i) const
Definition overworld.h:737
Which graphics sheets exist, which tables use them, and which 16x16 blocks are free.
absl::StatusOr< ExactDecodeResult > DecompressExact(const uint8_t *data, size_t data_size, size_t offset, size_t max_output, bool big_endian_copy)
Decodes one LC-LZ2 stream with the semantics of the ALTTP bank-00 decompressor (Decompress at $00:E79...
std::string ComputeSha1Hex(const uint8_t *data, size_t size)
Compute SHA-1 hash of data, return lowercase hex string (40 chars).
Definition rom_hash.cc:234
std::vector< std::vector< std::string > > ParseCsv(const std::string &text)
Zelda 3 specific classes and functions.
constexpr int kNumDungeonRooms
absl::StatusOr< RoomsetUsageReport > DescribeRoomsetUsage(Rom &rom, GameData &game_data, int roomset, const std::map< int, RoomGfxInfo > &rooms, const Overworld *live_overworld)
GfxGroupUsage FindSpritesetUsage(int spriteset, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms)
absl::StatusOr< MainGroupUsageReport > DescribeMainGroupUsage(Rom &rom, GameData &game_data, int group, int slot, const std::map< int, RoomGfxInfo > &rooms, const std::map< int, uint8_t > &explicit_room_main, const Overworld *live_overworld)
constexpr uint32_t kNumRoomBlocksets
Definition game_data.h:33
constexpr size_t kGfxSheet3bppBytes
nlohmann::json json
std::map< int, OverworldAreaGfxInfo > CollectOverworldAreaGfx(Rom &rom, GameData *game_data)
const char * GfxSheetInventoryKindName(GfxSheetStorageKind kind)
absl::StatusOr< GfxSheetInventory > BuildGfxSheetInventory(const Rom &rom, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms, const GfxSheetInventoryOptions &options)
constexpr uint16_t kDungeonSpritesetBase
constexpr uint16_t kSpriteSheetValueCount
std::map< int, RoomGfxInfo > CollectRoomGfx(const Rom &rom)
constexpr uint32_t kNumMainBlocksets
Definition game_data.h:32
constexpr uint16_t kSpriteSheetBase
GfxGroupUsage FindRoomsetUsage(int roomset, const std::map< int, OverworldAreaGfxInfo > &areas, const std::map< int, RoomGfxInfo > &rooms)
bool IsGfxSheetBlockEmpty(const std::vector< uint8_t > &data, int block, int bpp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr size_t kGfxSheetCount
constexpr int kGfxSheetBlockCount
16x16 blocks per sheet: 8 columns x 2 rows over the 128x32 sheet.
@ kVanilla
0xFF in ROM, no ZScream ASM applied
RoomMainBlocksetResolution ResolveRoomMainBlockset(int room_id, uint8_t header_blockset, uint8_t requested_main_blockset, const GameData &game_data)
std::map< uint8_t, std::set< std::string > > ParseSpritesetLabelCsv(const std::string &csv_text)
constexpr int kNumOverworldAreas
nlohmann::json GfxSheetInventoryToJson(const GfxSheetInventory &inventory)
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
absl::StatusOr< uint32_t > ReadGfxSheetPc(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Resolves a sheet's pointer. Does not read the sheet data.
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
Where one spriteset or roomset is used.
std::array< std::vector< int >, 3 > ow_areas_by_state
std::map< uint8_t, std::set< std::string > > sprite_value_labels
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
std::vector< GfxSheetInventoryEntry > sheets
std::vector< std::array< uint8_t, 8 > > main_blocksets
std::vector< std::array< uint8_t, 4 > > spritesets
std::vector< std::array< uint8_t, 4 > > room_blocksets
std::map< int, OverworldAreaGfxInfo > overworld_areas
std::map< int, RoomGfxInfo > rooms
std::array< uint8_t, 16 > static_graphics
std::array< uint8_t, 3 > sprite_graphics