yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
terrain_catalog_commands.cc
Go to the documentation of this file.
2
3#include <fstream>
4#include <map>
5#include <memory>
6#include <optional>
7#include <sstream>
8
9#include "cli/util/hex_util.h"
10#include "core/rom_settings.h"
12#include "util/macro.h"
13#include "zelda3/game_data.h"
16
17namespace yaze::cli::handlers {
18namespace {
19using nlohmann::json;
20constexpr char kAdapter[] = "alttp.overworld.tile16.v1";
22 public:
23 RestoreAddresses() : saved_(core::RomSettings::Get().address_overrides()) {}
24 ~RestoreAddresses() { core::RomSettings::Get().SetAddressOverrides(saved_); }
25
26 private:
28};
29std::string Bytes(const std::vector<uint8_t>& bytes) {
30 return std::string(bytes.begin(), bytes.end());
31}
32} // namespace
33
35 const resources::ArgumentParser& parser) {
36 RETURN_IF_ERROR(parser.RequireArgs({"map", "out"}));
37 int map = -1;
38 if (!util::ParseHexString(*parser.GetString("map"), &map) || map < 0 ||
40 return absl::InvalidArgumentError(
41 "Map must be within this adapter's supported 0x00-0x9F range");
42 }
43 if (parser.HasFlag("write")) {
44 return absl::InvalidArgumentError(
45 "Terrain catalog export never writes ROM data");
46 }
47 return absl::OkStatus();
48}
49
51 Rom* rom, const resources::ArgumentParser& parser,
52 resources::OutputFormatter& formatter) {
53 const auto original_rom = core::TerrainFingerprint(Bytes(rom->vector()));
54 RestoreAddresses restore;
55 // Explicit project context or vanilla defaults; never inherit another ROM's
56 // process-global address overrides.
58 std::string catalog_path = parser.GetString("catalog").value_or("");
60 if (auto path = parser.GetString("project-file")) {
61 RETURN_IF_ERROR(project.Open(*path));
63 if (catalog_path.empty()) {
64 catalog_path = project.GetAbsolutePath(project.terrain_catalog_file);
65 }
66 }
67 std::optional<core::TerrainCatalog> catalog;
68 if (!catalog_path.empty()) {
69 std::ifstream file(catalog_path);
70 if (!file)
71 return absl::NotFoundError("Cannot open terrain catalog: " +
72 catalog_path);
73 std::ostringstream text;
74 text << file.rdbuf();
75 ASSIGN_OR_RETURN(auto parsed, core::TerrainCatalog::Parse(text.str()));
76 catalog = std::move(parsed);
77 }
78 int requested = 0;
79 util::ParseHexString(*parser.GetString("map"), &requested);
81 zelda3::LoadOptions options;
82 options.expand_rom = false;
83 RETURN_IF_ERROR(zelda3::LoadGameData(*rom, data, options));
84 auto ow = std::make_unique<zelda3::Overworld>(rom);
85 ow->SetGameData(&data);
86 RETURN_IF_ERROR(ow->Load(rom));
87 const int parent = ow->overworld_map(requested)->parent();
88 if (parent < 0 || parent >= zelda3::kNumOverworldMaps ||
89 parent / 64 != requested / 64) {
90 return absl::FailedPreconditionError(
91 "Invalid area parent/world relationship");
92 }
93 RETURN_IF_ERROR(ow->EnsureMapBuilt(parent));
94 const auto* map = ow->overworld_map(parent);
95 int width = 32, height = 32;
96 if (map->area_size() == zelda3::AreaSizeEnum::LargeArea ||
97 map->area_size() == zelda3::AreaSizeEnum::WideArea)
98 width = 64;
99 if (map->area_size() == zelda3::AreaSizeEnum::LargeArea ||
100 map->area_size() == zelda3::AreaSizeEnum::TallArea)
101 height = 64;
102 const int world = parent / 64;
103 const int origin_x = (parent % 8) * 32;
104 const int origin_y = ((parent % 64) / 8) * 32;
105 if (origin_x + width > 256 || origin_y + height > (world == 2 ? 128 : 256)) {
106 return absl::FailedPreconditionError("Area exceeds supported world bounds");
107 }
108 json profile = {{"adapter", kAdapter},
109 {"world", world},
110 {"phase", 0},
111 {"area_graphics", map->area_graphics()},
112 {"area_palette", map->area_palette()},
113 {"main_palette", map->main_palette()},
114 {"background_color", map->area_specific_bg_color()},
115 {"subscreen_overlay", map->subscreen_overlay()},
116 {"animated_gfx", map->animated_gfx()},
117 {"graphics_fingerprint",
118 core::TerrainFingerprint(Bytes(map->current_graphics()))},
119 {"runtime_overrides", "not_applied"}};
120 profile["sheets"] = json::array();
121 for (int i = 0; i < 16; ++i)
122 profile["sheets"].push_back(map->static_graphics(i));
123 profile["palette"] = json::array();
124 for (const auto& color : map->current_palette())
125 profile["palette"].push_back(color.snes());
126 if (map->current_graphics().empty() || map->current_palette().empty()) {
127 return absl::FailedPreconditionError(
128 "Map graphics or palette did not resolve");
129 }
130 const auto profile_id = core::TerrainFingerprint(profile.dump());
131 std::map<int, json> occurrences;
132 auto& tiles = ow->GetMapTiles(world);
133 for (int y = 0; y < height; ++y) {
134 for (int x = 0; x < width; ++x) {
135 const int id = tiles[origin_x + x][origin_y + y];
136 if (id >= static_cast<int>(ow->tiles16().size())) {
137 return absl::DataLossError(
138 "Map references an unavailable Tile16 definition");
139 }
140 if (!occurrences.count(id))
141 occurrences[id] = json::array();
142 occurrences[id].push_back({x, y});
143 }
144 }
145 json records = json::array();
146 const auto types = ow->all_tiles_types();
147 for (const auto& [id, uses] : occurrences) {
148 const auto& tile = ow->tiles16()[id];
149 const gfx::TileInfo quadrants[] = {tile.tile0_, tile.tile1_, tile.tile2_,
150 tile.tile3_};
151 json pieces = json::array();
152 std::array<uint8_t, 256> transparency{};
153 int quadrant_index = 0;
154 std::string definition;
155 for (const auto& q : quadrants) {
156 const uint16_t word = gfx::TileInfoToWord(q);
157 definition.push_back(static_cast<char>(word & 255));
158 definition.push_back(static_cast<char>(word >> 8));
159 json piece = {{"tile8_id", q.id_},
160 {"palette", q.palette_},
161 {"priority", q.over_},
162 {"flip_x", q.horizontal_mirror_},
163 {"flip_y", q.vertical_mirror_},
164 {"raw_word", word},
165 {"tile_type", nullptr}};
166 if (q.id_ < types.size())
167 piece["tile_type"] = types[q.id_];
168 int transparent = 0;
171 data.graphics_buffer, map->static_graphics(q.id_ / 64),
172 q.id_, q.horizontal_mirror_, q.vertical_mirror_));
173 for (int py = 0; py < 8; ++py) {
174 for (int px = 0; px < 8; ++px) {
175 const auto zero = mask[py * 8 + px];
176 transparent += zero;
177 const int destination =
178 (quadrant_index / 2 * 8 + py) * 16 + quadrant_index % 2 * 8 + px;
179 transparency[destination] = zero;
180 }
181 }
182 piece["transparent_pixels"] = transparent;
183 pieces.push_back(piece);
184 ++quadrant_index;
185 }
186 const auto definition_id = core::TerrainFingerprint(definition);
187 json labels = json::array();
188 if (catalog)
189 labels = catalog->Resolve(kAdapter, id, definition_id, profile_id);
190 json pixels = json::array();
191 const auto& blockset = map->current_tile16_blockset();
192 for (int py = 0; py < 16; ++py) {
193 for (int px = 0; px < 16; ++px) {
194 const size_t offset = (id / 8) * 2048 + (id % 8) * 16 + py * 128 + px;
195 if (offset >= blockset.size())
196 return absl::DataLossError("Tile16 preview unavailable");
197 pixels.push_back(blockset[offset]);
198 }
199 }
200 records.push_back({{"tile_id", id},
201 {"pixels", pixels},
202 {"transparent_mask", transparency},
203 {"definition_fingerprint", definition_id},
204 {"quadrants", pieces},
205 {"occurrences", uses},
206 {"labels", labels}});
207 }
208 json output = {
209 {"schema_version", 1},
210 {"adapter", kAdapter},
211 {"transparency_basis", "decoded_source_zero_before_palette_remapping"},
212 {"rom_fingerprint", core::TerrainFingerprint(Bytes(rom->vector()))},
213 {"requested_map", requested},
214 {"parent_area", parent},
215 {"width_tiles", width},
216 {"height_tiles", height},
217 {"expanded_tile16", ow->expanded_tile16()},
218 {"profile_fingerprint", profile_id},
219 {"profile", profile},
220 {"coverage",
221 "selected parent area only; not a global collision impact report"},
222 {"collision_status",
223 "raw Tile8 lookup only; runtime hooks and high IDs unresolved"},
224 {"records", records}};
225 const std::string path = *parser.GetString("out");
226 if (core::TerrainFingerprint(Bytes(rom->vector())) != original_rom) {
227 return absl::InternalError(
228 "Read-only catalog extraction changed the in-memory ROM");
229 }
230 // Exclusive creation protects existing ROMs, catalogs and evidence artifacts.
231#ifndef __EMSCRIPTEN__
233 project::WriteProjectFileAtomically(path, output.dump(2) + "\n", false));
234#else
235 return absl::UnimplementedError(
236 "Terrain catalog file export requires a native build");
237#endif
238 formatter.AddField("output", path);
239 formatter.AddField("tiles", static_cast<int>(records.size()));
240 formatter.AddField("parent_area", parent);
241 formatter.AddField("rom_modified", false);
242 return absl::OkStatus();
243}
244} // namespace yaze::cli::handlers
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
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
Utility for consistent output formatting across commands.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
static RomSettings & Get()
void SetAddressOverrides(const RomAddressOverrides &overrides)
static absl::StatusOr< TerrainCatalog > Parse(std::string_view text)
SNES 16-bit tile metadata container.
Definition snes_tile.h:52
struct destination destination
Room transition destination.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string TerrainFingerprint(std::string_view bytes)
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:362
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:387
bool ParseHexString(absl::string_view str, uint64_t *out)
Definition hex.cc:133
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
constexpr int kNumOverworldMaps
Definition common.h:85
absl::StatusOr< std::array< uint8_t, 64 > > Tile8SourceTransparency(const std::vector< uint8_t > &decoded, int sheet, int tile_id, bool flip_x, bool flip_y)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
std::string terrain_catalog_file
Definition project.h:336
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2028
absl::Status Open(const std::string &project_path)
Definition project.cc:537
core::RomAddressOverrides rom_address_overrides
Definition project.h:351
std::vector< uint8_t > & graphics_buffer
Legacy name for sheet_store.mutable_pixels().