9#include "absl/strings/str_format.h"
11#include "nlohmann/json.hpp"
32bool Contains(
const std::array<uint8_t, N>& values,
int value) {
33 return std::find(values.begin(), values.end(), value) != values.end();
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;
42 for (
size_t i = 0; i < text.size(); ++i) {
43 const char c = text[i];
45 if (c ==
'"' && i + 1 < text.size() && text[i + 1] ==
'"') {
48 }
else if (c ==
'"') {
53 }
else if (c ==
'"') {
55 }
else if (c ==
',') {
56 row.push_back(std::move(field));
58 }
else if (c ==
'\n' || c ==
'\r') {
59 if (c ==
'\r' && i + 1 < text.size() && text[i + 1] ==
'\n') {
62 row.push_back(std::move(field));
64 rows.push_back(std::move(row));
70 if (!field.empty() || !row.empty()) {
71 row.push_back(std::move(field));
72 rows.push_back(std::move(row));
80 std::vector<int> free;
87 free.push_back(block);
94 const Rom& rom,
const std::map<int, OverworldAreaGfxInfo>& areas,
95 const std::map<int, RoomGfxInfo>& rooms,
98 return absl::FailedPreconditionError(
"ROM not loaded");
100 const auto& bytes = rom.
vector();
103 inventory.
rooms = rooms;
108 auto& entry = inventory.
sheets[gfx];
119 entry.reserved_blocks = it->second;
125 entry.error = std::string(pc.status().message());
130 std::vector<uint8_t> data;
133 entry.error =
"raw sheet runs past the ROM end";
136 data.assign(bytes.begin() + entry.pc,
141 entry.pc, kMeasureLimit,
144 entry.error = std::string(decoded.status().message());
147 entry.stored_bytes = decoded->compressed_size;
148 data = std::move(decoded->data);
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; })) {
164 entry.empty_16x16_blocks.push_back(block);
167 entry.all_empty = entry.empty_8x8 == 64;
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);
178 if (!entry.sha1.empty() && !entry.all_empty) {
179 by_sha[entry.sha1].push_back(entry.gfx);
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);
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);
198 auto read = [&bytes](
size_t pc) -> uint8_t {
199 return pc < bytes.size() ? bytes[pc] : 0;
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);
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);
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);
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) {
231 use.main_blocksets.push_back(n);
234 for (
int n = 0; n < kRoomBlocksetCount; ++n) {
236 use.room_blocksets.push_back(n);
239 for (
const auto& [area, info] : areas) {
240 if (Contains(info.static_graphics, gfx)) {
241 use.ow_areas_static.push_back(area);
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);
251 auto uses_set = [&use](
int set) {
252 return std::binary_search(use.spritesets.begin(), use.spritesets.end(),
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);
261 for (
const auto& [room, info] : rooms) {
263 use.rooms_sprite.push_back(room);
268 entry.labels.assign(it->second.begin(), it->second.end());
271 entry.unreferenced = !use.Any() && !entry.engine_loaded;
278 std::map<int, OverworldAreaGfxInfo> areas;
287 info.
world = index < 0x40 ? 0 : (index < 0x80 ? 1 : 2);
290 for (
int i = 0; i < 3; ++i) {
293 for (
int i = 0; i < 16; ++i) {
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());
315 const int64_t slot =
static_cast<int64_t
>(table) + room * 2;
316 if (!in_range(slot, 2)) {
320 (bytes[slot + 1] << 8) | bytes[slot]);
322 if (!in_range(header, 14)) {
325 rooms[room] = {bytes[header + 2], bytes[header + 3]};
331 int spriteset,
const std::map<int, OverworldAreaGfxInfo>& areas,
332 const std::map<int, RoomGfxInfo>& rooms) {
334 for (
const auto& [area, info] : areas) {
335 for (
int state = 0; state < 3; ++state) {
336 if (info.sprite_graphics[state] == spriteset) {
341 for (
const auto& [room, info] : rooms) {
343 usage.
rooms.push_back(room);
350 const std::map<int, OverworldAreaGfxInfo>& areas,
351 const std::map<int, RoomGfxInfo>& rooms) {
353 for (
const auto& [area, info] : areas) {
354 if (info.area_graphics == roomset) {
358 for (
const auto& [room, info] : rooms) {
359 if (info.blockset == roomset) {
360 usage.
rooms.push_back(room);
368 const std::map<int, RoomGfxInfo>& rooms,
const Overworld* live_overworld) {
370 return absl::FailedPreconditionError(
"ROM not loaded");
373 return absl::InvalidArgumentError(
"Invalid roomset index");
376 return absl::FailedPreconditionError(
"Graphics groups are not loaded");
378 if (live_overworld && live_overworld->
rom() != &rom) {
379 return absl::FailedPreconditionError(
380 "Live overworld belongs to a different ROM");
385 live_overworld !=
nullptr && live_overworld->
is_loaded();
386 std::map<int, RoomGfxInfo> scanned_rooms;
387 for (
const auto& [
id, info] : rooms) {
389 scanned_rooms.emplace(
id, info);
394 std::map<int, OverworldAreaGfxInfo> scanned_areas;
395 std::map<int, OverworldRoomsetSource> sources;
398 const bool can_scan_rom =
411 scanned_areas.emplace(
id, info);
412 sources.emplace(
id, map->roomset_source());
414 }
else if (can_scan_rom) {
419 scanned_areas.emplace(
id, info);
429 switch (sources.at(
id)) {
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,
450 return absl::FailedPreconditionError(
"ROM not loaded");
454 return absl::InvalidArgumentError(
"Invalid main group or slot index");
457 return absl::FailedPreconditionError(
"Graphics groups are not loaded");
459 if (live_overworld && live_overworld->
rom() != &rom) {
460 return absl::FailedPreconditionError(
461 "Live overworld belongs to a different ROM");
467 live_overworld !=
nullptr && live_overworld->
is_loaded();
468 int scanned_rooms = 0;
469 for (
const auto& [
id, info] : 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 =
479 if (resolved.main_blockset != group) {
482 if (slot >= 3 && slot <= 6 &&
488 switch (resolved.source) {
506 const bool can_scan_rom =
511 int scanned_ow_areas = 0;
512 auto record_area = [&](
int id,
const OverworldMap& map) {
514 const auto provenance = map.MainSlotProvenance(slot);
515 if (provenance.main_group_id != group) {
518 switch (provenance.source) {
534 record_area(
id, *map);
537 }
else if (can_scan_rom) {
540 record_area(
id, map);
560 using nlohmann::json;
561 json out = json::object();
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);
570 if (entry.sprite_value.has_value()) {
571 sheet[
"sprite_value"] = absl::StrFormat(
"0x%02X", *entry.sprite_value);
573 if (entry.error.empty()) {
574 sheet[
"stored_bytes"] = entry.stored_bytes.has_value()
575 ?
json(*entry.stored_bytes)
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();
586 if (!entry.error.empty()) {
587 sheet[
"error"] = entry.error;
589 if (entry.reserved) {
590 sheet[
"reserved"] =
"reserved by project config";
593 sheet[
"flagged"] =
"needs owner approval before use";
595 if (!entry.reserved_blocks.empty()) {
596 sheet[
"reserved_blocks"] = entry.reserved_blocks;
598 if (entry.engine_loaded) {
599 sheet[
"engine_loaded"] =
true;
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;
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;
614 sheet[
"usage"] = std::move(usage);
615 if (!entry.labels.empty()) {
616 sheet[
"labels"] = entry.labels;
618 sheet[
"unreferenced"] = entry.unreferenced;
619 sheets.push_back(std::move(sheet));
621 out[
"sheets"] = std::move(sheets);
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());
631 out[
"spritesets"] = table(inventory.
spritesets);
635 static constexpr const char* kWorlds[] = {
"Light",
"Dark",
"Special"};
636 json areas = json::object();
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)},
648 out[
"overworld_areas"] = std::move(areas);
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}};
655 out[
"rooms"] = std::move(rooms);
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) {
666 if (!std::regex_search(row[column], match, kCell)) {
669 std::string
name = match[2].str();
670 while (!
name.empty() &&
671 std::isspace(
static_cast<unsigned char>(
name.back()))) {
675 static_cast<uint8_t
>(std::stoi(match[1].str(),
nullptr, 16));
676 labels[value].insert(std::move(
name));
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
const auto & vector() const
Represents a single Overworld map screen.
auto area_graphics() const
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 overworld_map(int i) const
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).
constexpr int kMainBlocksetCount
std::vector< std::vector< std::string > > ParseCsv(const std::string &text)
constexpr uint32_t kRoomBlocksetTable
constexpr int kSpritesetCount
constexpr int kRoomBlocksetCount
constexpr size_t kTileBytes3bpp
constexpr size_t kMeasureLimit
constexpr uint32_t kSpritesetTable
constexpr uint32_t kMainBlocksetPointer
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
constexpr size_t kGfxSheet3bppBytes
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
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
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Where one spriteset or roomset is used.
std::array< std::vector< int >, 3 > ow_areas_by_state
std::vector< int > ow_areas
std::vector< int > empty_16x16_blocks
std::vector< uint16_t > reserved_blocks
std::vector< int > FreeBlocks() const
std::set< uint16_t > flagged_sheets
std::map< uint8_t, std::set< std::string > > sprite_value_labels
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks
std::set< uint16_t > reserved_sheets
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::vector< int > overridden_ow_areas
std::vector< int > independent_ow_areas
std::vector< int > inferred_default_rooms
std::vector< int > direct_default_rooms
std::vector< int > ow_areas
std::vector< int > fallback_rooms
std::vector< int > overridden_rooms
std::vector< int > explicit_preview_rooms
bool slot7_bottom_half_only
std::array< uint8_t, 16 > static_graphics
std::array< uint8_t, 3 > sprite_graphics
std::vector< int > overridden_ow_areas
std::vector< int > ow_areas
std::vector< int > independent_ow_areas