yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_render_commands.cc
Go to the documentation of this file.
2
3#include <array>
4#include <filesystem>
5#include <fstream>
6#include <iterator>
7#include <optional>
8#include <sstream>
9#include <string>
10#include <unordered_map>
11
12#include "absl/strings/numbers.h"
13#include "absl/strings/str_format.h"
15#include "cli/util/hex_util.h"
18#include "core/rom_settings.h"
20#include "nlohmann/json.hpp"
21#include "rom/rom.h"
22#include "util/macro.h"
23#include "zelda3/game_data.h"
26
27namespace yaze {
28namespace cli {
29namespace handlers {
30
31namespace {
32
33namespace ow = app::service::OverworldOverlay;
34
36 public:
38 : saved_(core::RomSettings::Get().address_overrides()) {
39 core::RomSettings::Get().SetAddressOverrides(project);
40 }
42 core::RomSettings::Get().SetAddressOverrides(saved_);
43 }
44
45 private:
47};
48
49absl::StatusOr<uint32_t> ParseOverworldOverlays(const std::string& s) {
50 uint32_t flags = ow::kNone;
51 std::istringstream ss(s);
52 std::string tok;
53 while (std::getline(ss, tok, ',')) {
54 tok.erase(0, tok.find_first_not_of(" \t"));
55 if (!tok.empty())
56 tok.erase(tok.find_last_not_of(" \t") + 1);
57 if (tok.empty())
58 continue;
59 if (tok == "sprites")
60 flags |= ow::kSprites;
61 else if (tok == "entrances")
62 flags |= ow::kEntrances;
63 else if (tok == "exits")
64 flags |= ow::kExits;
65 else if (tok == "holes")
66 flags |= ow::kHoles;
67 else if (tok == "items")
68 flags |= ow::kItems;
69 else if (tok == "grid")
70 flags |= ow::kGrid;
71 else if (tok == "all")
72 flags = ow::kAll & ~ow::kGrid; // grid stays opt-in
73 else
74 return absl::InvalidArgumentError(
75 absl::StrFormat("Unknown overlay '%s' (expected sprites, entrances, "
76 "exits, holes, items, grid, all)",
77 tok));
78 }
79 return flags;
80}
81
82absl::StatusOr<bool> ParseAreaOverlay(const std::string& value) {
83 if (value == "on" || value == "true" || value == "1")
84 return true;
85 if (value == "off" || value == "false" || value == "0")
86 return false;
87 return absl::InvalidArgumentError("--area-overlay must be on or off");
88}
89
90std::optional<std::string> OutputArg(const resources::ArgumentParser& parser) {
91 if (auto out = parser.GetString("out"); out.has_value())
92 return out;
93 return parser.GetString("output");
94}
95
96} // namespace
97
99 Rom* rom, const resources::ArgumentParser&,
100 resources::OutputFormatter& formatter) {
101 if (!rom || !rom->is_loaded())
102 return absl::FailedPreconditionError("ROM not loaded");
103 if (!project_ || !project_->project_opened())
104 return absl::FailedPreconditionError(
105 "Lyra build verification requires --project-context");
106 if (project_->lyra_terrain_file.empty())
107 return absl::FailedPreconditionError(
108 "Save the project's Lyra terrain before building");
109
110 const auto source_hash = core::ComputeSourceArtifactSha256(
111 {reinterpret_cast<const char*>(rom->data()), rom->size()});
112 ScopedRomReadOnly read_only(*rom);
113 ScopedLyraRomAddresses addresses(project_->rom_address_overrides);
114
115 const auto terrain_path =
117 std::error_code ec;
118 const auto size = std::filesystem::file_size(terrain_path, ec);
119 if (ec || size > 4 * 1024 * 1024)
120 return absl::FailedPreconditionError(
121 "Lyra terrain is missing or exceeds 4 MiB");
122 std::ifstream file(terrain_path, std::ios::binary);
123 if (!file)
124 return absl::PermissionDeniedError("Cannot read Lyra terrain");
125 const std::string text{std::istreambuf_iterator<char>(file), {}};
126 if (file.bad())
127 return absl::DataLossError("Cannot finish reading Lyra terrain");
128
129 zelda3::GameData game_data;
130 zelda3::LoadOptions options;
131 options.expand_rom = false;
132 RETURN_IF_ERROR(zelda3::LoadGameData(*rom, game_data, options));
133 zelda3::Overworld overworld(rom, &game_data);
134 RETURN_IF_ERROR(overworld.Load(rom));
137 ASSIGN_OR_RETURN(auto document,
138 core::LyraTerrainDocument::Parse(text, profile));
139 std::unordered_map<std::string, std::string> native_bindings;
140 std::unordered_map<std::string, std::array<uint16_t, 256>> resolved_palettes;
141 std::unordered_map<std::string, nlohmann::json> resolved_assets;
142 auto inspected = nlohmann::json::array();
143 int mismatches = 0;
144 std::array<std::array<std::string, 3>, core::LyraTerrainDocument::kMapCount>
145 verified_bindings;
146 for (int i = 0; i < core::LyraTerrainDocument::kMapCount; ++i) {
147 const auto& map = document.authoring().maps[i];
148 for (int state = 0; state < 3; ++state) {
149 const auto key = zelda3::LyraMapAssetKey(map.properties, state);
150 auto found = native_bindings.find(key);
151 if (found == native_bindings.end()) {
153 overworld, map.properties, state));
154 if (InspectAssets()) {
155 ASSIGN_OR_RETURN(const auto description,
157 state, assets));
158 resolved_assets.emplace(key, nlohmann::json::parse(description));
159 std::array<uint16_t, 256> palette;
160 for (size_t color = 0; color < palette.size(); ++color)
161 palette[color] = assets.source.current_palette()[color].snes();
162 resolved_palettes.emplace(key, palette);
163 }
164 found = native_bindings.emplace(key, std::move(assets.binding)).first;
165 }
166 const auto& expected = map.asset_bindings[state];
167 const bool changed = !expected.empty() && expected != found->second;
168 if (changed) {
169 ++mismatches;
170 if (!InspectAssets())
171 return absl::FailedPreconditionError(absl::StrFormat(
172 "Lyra map $%02X state %d native asset binding changed",
174 }
175 if (InspectAssets()) {
176 inspected.push_back({
178 {"state", state},
179 {"stored_binding", expected},
180 {"current_binding", found->second},
181 {"binding_changed", changed},
182 {"palette_words", resolved_palettes.at(key)},
183 {"native_base_binding", found->second},
184 {"runtime_assignment", "unassigned"},
185 {"native_assets", resolved_assets.at(key)},
186 });
187 }
188 // Default properties bind through the verified shared profile. Preserve
189 // their empty representation for the compiler's existing input contract.
190 verified_bindings[i][state] = expected.empty() ? "" : found->second;
191 }
192 }
193
194 if (source_hash !=
196 {reinterpret_cast<const char*>(rom->data()), rom->size()}))
197 return absl::DataLossError("Lyra verification changed the source ROM");
198 formatter.AddField("source_rom_sha256", source_hash);
199 formatter.AddField("terrain_sha256", core::ComputeSourceArtifactSha256(text));
200 formatter.AddField("terrain_path", terrain_path);
201 formatter.AddField("map_count", core::LyraTerrainDocument::kMapCount);
202 formatter.AddField("donor_map", 0x3A);
203 formatter.BeginObject("profile");
204 std::ostringstream sheets;
205 sheets << '[';
206 for (size_t i = 0; i < profile.graphics_sheets.size(); ++i) {
207 if (i)
208 sheets << ',';
209 sheets << static_cast<int>(profile.graphics_sheets[i]);
210 }
211 sheets << ']';
212 formatter.AddRawJsonField("graphics_sheets", sheets.str());
213 formatter.AddField("main_palette", profile.main_palette);
214 formatter.AddField("palette_set", profile.palette_set);
215 formatter.AddField("animated_sheet", profile.animated_sheet);
216 formatter.AddField("tile_count", static_cast<int>(profile.tile_count));
217 formatter.AddField("tile16_definition_fingerprint",
219 formatter.AddField("graphics_fingerprint", profile.graphics_fingerprint);
220 formatter.AddField("palette_fingerprint", profile.palette_fingerprint);
221 formatter.AddField("animation_fingerprint", profile.animation_fingerprint);
222 formatter.EndObject();
223
224 if (InspectAssets()) {
225 formatter.AddField("inspection_only", true);
226 formatter.AddField("native_resolution_schema",
227 std::string("yaze.lyra-native-resolution.v1"));
228 formatter.AddField("unique_native_resolutions",
229 static_cast<int>(resolved_assets.size()));
230 // Capture the exact effective map, including unknown keys. Default JSON
231 // objects sort keys, so dump() is canonical across unordered-map insertion.
232 nlohmann::json overrides = nlohmann::json::object();
233 for (const auto& [key, value] : project_->rom_address_overrides.addresses)
234 overrides[key] = value;
235 formatter.AddRawJsonField("rom_address_overrides", overrides.dump());
236 formatter.AddField("rom_address_overrides_sha256",
237 core::ComputeSourceArtifactSha256(overrides.dump()));
238 formatter.AddField(
239 "rom_address_overrides_encoding",
240 std::string(
241 "compact sorted-key JSON; uint32 decimal values; no newline"));
242 const auto descriptor_hash =
244 formatter.AddField("project_descriptor_path", project_->filepath);
245 formatter.AddRawJsonField("project_descriptor_sha256",
246 descriptor_hash.ok()
247 ? nlohmann::json(*descriptor_hash).dump()
248 : "null");
249 formatter.AddField(
250 "project_descriptor_identity_status",
251 std::string(descriptor_hash.ok() ? "identified" : "unavailable"));
252 if (!descriptor_hash.ok())
253 formatter.AddField("project_descriptor_identity_error",
254 std::string(descriptor_hash.status().message()));
255 formatter.AddField("asset_binding_mismatches", mismatches);
256 formatter.AddRawJsonField("map_assets", inspected.dump());
257 return absl::OkStatus();
258 }
259
260 auto native_99 = *overworld.mutable_overworld_map(0x99);
261 native_99.set_game_state(0);
262 native_99.LoadAreaGraphics();
263 formatter.BeginObject("native_99");
264 std::ostringstream native_sheets;
265 native_sheets << '[';
266 for (int i = 0; i < 8; ++i) {
267 if (i)
268 native_sheets << ',';
269 native_sheets << static_cast<int>(native_99.static_graphics(i));
270 }
271 native_sheets << ']';
272 formatter.AddRawJsonField("graphics_sheets", native_sheets.str());
273 formatter.AddField("main_palette", native_99.render_main_palette());
274 formatter.AddField("palette_set", native_99.area_palette());
275 formatter.AddField("animated_sheet", native_99.animated_sheet());
276 formatter.AddField("backdrop_color", native_99.area_specific_bg_color());
277 formatter.EndObject();
278 std::string bindings = "[";
279 for (int i = 0; i < core::LyraTerrainDocument::kMapCount; ++i) {
280 if (i)
281 bindings += ',';
282 bindings += '[';
283 for (int state = 0; state < 3; ++state) {
284 if (state)
285 bindings += ',';
286 bindings += '"' + verified_bindings[i][state] + '"';
287 }
288 bindings += ']';
289 }
290 formatter.AddRawJsonField("verified_asset_bindings", bindings + "]");
291 std::ostringstream definitions;
292 definitions << '[';
293 const auto native = overworld.tiles32_unique();
294 for (size_t i = 0; i < native.size(); ++i) {
295 if (i)
296 definitions << ',';
297 const auto& tile = native[i];
298 definitions << '[' << tile.tile0_ << ',' << tile.tile1_ << ','
299 << tile.tile2_ << ',' << tile.tile3_ << ']';
300 }
301 definitions << ']';
302 formatter.AddRawJsonField("native_definitions", definitions.str());
303 formatter.AddField("native_tile32_capacity", static_cast<int>(native.size()));
304 return absl::OkStatus();
305}
306
307absl::StatusOr<OverworldRenderGraphicsPreview>
309 const std::optional<std::string>& flag) {
311 const std::string value = flag.value_or("");
312 if (value == "off" || value == "false" || value == "0") {
313 result.mode = "off";
314 return result;
315 }
316 const bool required = value == "on" || value == "true" || value == "1";
317 if (!value.empty() && !required) {
318 result.mode = "file";
319 result.manifest = value;
320 } else if (project != nullptr &&
321 !project->overworld_graphics_preview.empty()) {
322 result.mode = "project";
323 result.manifest =
325 } else if (required) {
326 return absl::FailedPreconditionError(
327 "--graphics-preview=on needs --project-context with [files] "
328 "overworld_graphics_preview");
329 } else {
330 result.mode = "none";
331 return result;
332 }
333 auto preview = core::LoadOverworldGraphicsPreview(result.manifest);
334 if (!preview.ok()) {
335 return absl::Status(
336 preview.status().code(),
337 absl::StrFormat("--graphics-preview %s: %s", result.manifest,
338 preview.status().message()));
339 }
340 result.preview = std::move(preview).value();
341 return result;
342}
343
345 const resources::ArgumentParser& parser) {
346 RETURN_IF_ERROR(parser.RequireArgs({"screen"}));
347 const auto out = OutputArg(parser);
348 if (!out.has_value() || out->empty()) {
349 return absl::InvalidArgumentError("--out <path.png> is required");
350 }
351 if (const auto scale = parser.GetString("scale"); scale.has_value()) {
353 }
354 if (const auto ov = parser.GetString("overlays"); ov.has_value()) {
355 RETURN_IF_ERROR(ParseOverworldOverlays(*ov).status());
356 }
357 if (const auto ao = parser.GetString("area-overlay"); ao.has_value()) {
358 RETURN_IF_ERROR(ParseAreaOverlay(*ao).status());
359 }
360 if (const auto gp = parser.GetString("graphics-preview");
361 gp.has_value() && gp->empty()) {
362 return absl::InvalidArgumentError(
363 "--graphics-preview must be on, off, or a manifest path");
364 }
365 if (const auto phase = parser.GetString("phase"); phase.has_value()) {
366 int value = -1;
367 if (!absl::SimpleAtoi(*phase, &value) || value < 0 || value > 2) {
368 return absl::InvalidArgumentError("--phase must be 0, 1, or 2");
369 }
370 }
371 return absl::OkStatus();
372}
373
375 Rom* rom, const resources::ArgumentParser& parser,
376 resources::OutputFormatter& formatter) {
377 resources::CommandInvocationContext invocation_context;
378 if (rom != nullptr && !rom->filename().empty()) {
379 invocation_context.source_rom_path = std::filesystem::path(rom->filename());
380 invocation_context.active_rom_path = std::filesystem::path(rom->filename());
381 }
382 return ExecuteWithContext(rom, parser, formatter, invocation_context);
383}
384
386 Rom* rom, const resources::ArgumentParser& parser,
388 const resources::CommandInvocationContext& invocation_context) {
390
391 int screen = 0;
392 if (!util::ParseHexString(*parser.GetString("screen"), &screen)) {
393 return absl::InvalidArgumentError("--screen must be hex (e.g. 0x40)");
394 }
395
396 const std::string output_path = *OutputArg(parser);
397 ASSIGN_OR_RETURN(const auto resolved_output,
400 invocation_context));
401
403 req.screen_id = screen;
404 if (auto ov = parser.GetString("overlays"); ov.has_value()) {
405 ASSIGN_OR_RETURN(req.overlay_flags, ParseOverworldOverlays(*ov));
406 }
407 if (auto phase = parser.GetString("phase"); phase.has_value()) {
408 absl::SimpleAtoi(*phase, &req.phase);
409 }
410 if (auto sc = parser.GetString("scale"); sc.has_value()) {
412 }
413 if (auto ao = parser.GetString("area-overlay"); ao.has_value()) {
414 ASSIGN_OR_RETURN(req.area_overlay, ParseAreaOverlay(*ao));
415 }
416 ASSIGN_OR_RETURN(auto graphics_preview,
418 project_, parser.GetString("graphics-preview")));
419 req.graphics_preview = std::move(graphics_preview.preview);
420
421 if (!rom || !rom->is_loaded()) {
422 return absl::FailedPreconditionError("ROM not loaded");
423 }
424
425 zelda3::GameData game_data;
426 auto gd_status = zelda3::LoadGameData(*rom, game_data);
427 if (!gd_status.ok()) {
428 return absl::InternalError(
429 absl::StrFormat("Failed to load game data: %s", gd_status.message()));
430 }
431
432 app::service::RenderService render_service(rom, &game_data);
433 ASSIGN_OR_RETURN(auto result, render_service.RenderOverworldArea(req));
434
436 invocation_context));
437 std::ofstream out(resolved_output, std::ios::binary);
438 if (!out) {
439 return absl::InternalError(
440 absl::StrFormat("Cannot open output file: %s", output_path));
441 }
442 out.write(reinterpret_cast<const char*>(result.png_data.data()),
443 static_cast<std::streamsize>(result.png_data.size()));
444 out.close();
445 if (!out) {
446 return absl::InternalError(
447 absl::StrFormat("Write failed for: %s", output_path));
448 }
449
450 formatter.AddField("requested_screen",
451 absl::StrFormat("0x%02X", result.requested_screen));
452 formatter.AddField("parent_screen",
453 absl::StrFormat("0x%02X", result.parent_screen));
454 formatter.AddField("area_size", result.area_size);
455 formatter.AddField("subscreen_overlay",
456 result.subscreen_overlay < 0
457 ? std::string("none")
458 : absl::StrFormat("0x%02X", result.subscreen_overlay));
459 formatter.AddField("graphics_preview", graphics_preview.mode);
460 if (!graphics_preview.manifest.empty()) {
461 formatter.AddField("graphics_preview_manifest", graphics_preview.manifest);
462 }
463 formatter.AddField("graphics_preview_applied", result.graphics_preview);
464 formatter.BeginArray("screens");
465 for (int s : result.screens)
466 formatter.AddArrayItem(absl::StrFormat("0x%02X", s));
467 formatter.EndArray();
468 formatter.AddField("sprite_phase", req.phase < 0
469 ? std::string("all")
470 : absl::StrFormat("%d", req.phase));
471 formatter.AddField("output", output_path);
472 formatter.AddField("width", result.width);
473 formatter.AddField("height", result.height);
474 formatter.AddField("scale", absl::StrFormat("%.2f", req.scale));
475 formatter.AddField("size_bytes", static_cast<int>(result.png_data.size()));
476 formatter.AddField(
477 "coordinates",
478 "marker x/y are area-local pixels at scale 1; tile = x/16, y/16");
479 formatter.BeginArray("markers");
480 for (const auto& m : result.markers) {
481 formatter.BeginObject();
482 formatter.AddField("kind", m.kind);
483 formatter.AddField("id", absl::StrFormat("0x%02X", m.id));
484 if (m.phase >= 0) {
485 formatter.AddField("phase", m.phase);
486 formatter.AddField("list_index", m.list_index);
487 }
488 formatter.AddField("x", m.x);
489 formatter.AddField("y", m.y);
490 formatter.AddField("tile", absl::StrFormat("(%d,%d)", m.x / 16, m.y / 16));
491 formatter.EndObject();
492 }
493 formatter.EndArray();
494 return absl::OkStatus();
495}
496
497} // namespace handlers
498} // namespace cli
499} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
auto filename() const
Definition rom.h:219
auto data() const
Definition rom.h:213
auto size() const
Definition rom.h:212
bool is_loaded() const
Definition rom.h:199
absl::StatusOr< OverworldRenderResult > RenderOverworldArea(const OverworldRenderRequest &req)
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status ValidateArgs(const resources::ArgumentParser &parser) override
Validate command arguments.
absl::Status ExecuteWithContext(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, const resources::CommandInvocationContext &invocation_context) override
Execute with immutable, invocation-scoped ROM path identity.
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)
absl::Status RequireArgs(const std::vector< std::string > &required) const
Validate that required arguments are present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddRawJsonField(const std::string &key, const std::string &json)
Add a field whose value is already-serialized JSON (numbers, nested arrays, null)....
void AddArrayItem(const std::string &item)
Add an item to current array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
static absl::StatusOr< LyraTerrainDocument > Parse(std::string_view text, const LyraTerrainProfile &expected_profile)
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:158
auto mutable_overworld_map(int i)
Definition overworld.h:776
auto tiles32_unique() const
Definition overworld.h:785
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
std::optional< std::string > OutputArg(const resources::ArgumentParser &parser)
absl::StatusOr< OverworldRenderGraphicsPreview > ResolveOverworldRenderGraphicsPreview(const project::YazeProject *project, const std::optional< std::string > &flag)
absl::StatusOr< std::filesystem::path > ResolveStableArtifactPath(const std::filesystem::path &path)
absl::Status RejectArtifactRomAliases(absl::string_view option_name, const std::filesystem::path &artifact_path, const CommandInvocationContext &invocation_context)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::string > ComputeSourceArtifactFileSha256(const fs::path &path)
absl::StatusOr< zelda3::OverworldGraphicsPreview > LoadOverworldGraphicsPreview(const std::string &filename)
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
absl::StatusOr< std::string > DescribeLyraNativeMapAssets(const core::LyraMapProperties &p, int state, const LyraNativeMapAssets &assets)
std::string LyraMapAssetKey(const core::LyraMapProperties &p, int state)
absl::StatusOr< LyraNativeMapAssets > ResolveLyraMapAssets(Overworld &overworld, const core::LyraMapProperties &p, int state)
absl::Status BindLyraTerrainSource(Overworld &overworld, core::LyraTerrainProfile *profile, gfx::Bitmap *atlas)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
zelda3::OverworldGraphicsPreview graphics_preview
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path
std::array< uint8_t, 8 > graphics_sheets
std::unordered_map< std::string, uint32_t > addresses
Modern project structure with comprehensive settings consolidation.
Definition project.h:290
bool project_opened() const
Definition project.h:538
std::string lyra_terrain_file
Definition project.h:342
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
std::string overworld_graphics_preview
Definition project.h:323
core::RomAddressOverrides rom_address_overrides
Definition project.h:367