yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_wall_profile_commands.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <optional>
5#include <set>
6#include <string>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
12#include "util/macro.h"
14
15namespace yaze::cli::handlers {
16namespace {
17
18absl::StatusOr<zelda3::WallProfileContext> ParseContext(
19 const resources::ArgumentParser& parser) {
21 ASSIGN_OR_RETURN(context.room, parser.GetInt("room"));
22 if (parser.GetString("world")) {
23 int world;
24 ASSIGN_OR_RETURN(world, parser.GetInt("world"));
25 context.world = world;
26 }
27 if (parser.GetString("entrance")) {
28 int entrance;
29 ASSIGN_OR_RETURN(entrance, parser.GetInt("entrance"));
30 context.entrance = entrance;
31 }
32 if (context.room < 0 || context.room > 0x127 ||
33 (context.world && (*context.world < 0 || *context.world > 1)) ||
34 (context.entrance &&
35 (*context.entrance < 0 || *context.entrance > 0x84))) {
36 return absl::InvalidArgumentError("Wall profile context is out of range");
37 }
38 return context;
39}
40
41absl::Status RequireMissing(const std::filesystem::path& path) {
42 std::error_code error;
43 const auto status = std::filesystem::symlink_status(path, error);
44 if (error && error != std::errc::no_such_file_or_directory) {
45 return absl::FailedPreconditionError("Cannot inspect preview output: " +
46 path.string());
47 }
48 if (std::filesystem::exists(status) || std::filesystem::is_symlink(status)) {
49 return absl::AlreadyExistsError(
50 "Preview output must be a fresh artifact: " + path.string());
51 }
52 return absl::OkStatus();
53}
54
55absl::Status ProtectOutput(
56 const std::filesystem::path& output,
58 const std::vector<std::filesystem::path>& inputs) {
60 resources::RejectArtifactRomAliases("--output", output, invocation));
61 for (const auto& input : inputs) {
62 bool alias;
63 ASSIGN_OR_RETURN(alias, resources::PathsAlias(output, input));
64 if (alias) {
65 return absl::InvalidArgumentError(
66 "Preview output aliases a protected input: " + input.string());
67 }
68 }
69 return absl::OkStatus();
70}
71
72std::string PreviewAsm(const zelda3::DungeonTilePatterns& patterns) {
73 std::string result =
74 "; Generated wall-profile PREVIEW overlay; not a room-scoped ROM patch.\n"
75 "lorom\n";
76 std::optional<uint32_t> previous;
77 for (const auto& [pc, word] : patterns.words) {
78 if (!previous || pc != *previous + 2) {
79 result += absl::StrFormat("org $%06X\n", pc + 0x8000);
80 }
81 result += absl::StrFormat(" dw $%04X\n", word);
82 previous = pc;
83 }
84 return result;
85}
86
87} // namespace
88
91 Descriptor descriptor;
92 descriptor.display_name = "Dungeon Wall Profile";
93 descriptor.summary =
94 "Validate and resolve a strict room-selected wall profile; optionally "
95 "emit a fresh read-only preview ASM, without changing ROM or project.";
96 descriptor.entries = {
97 {"--source", "Version-1 wall profile JSON collection", ""},
98 {"--room", "Room ID 0..0x127 (decimal or 0x-prefixed)", ""},
99 {"--world", "Optional explicit world 0 or 1", ""},
100 {"--entrance", "Optional explicit entrance 0..0x84", ""},
101 {"--legacy-source", "Literal legacy ASM; defaults to project descriptor",
102 ""},
103 {"--output", "Optional fresh preview ASM artifact; never overwritten",
104 ""},
105 };
106 return descriptor;
107}
108
110 const resources::ArgumentParser& parser) {
111 RETURN_IF_ERROR(parser.RequireArgs({"source", "room"}));
112 for (const auto* field :
113 {"source", "room", "world", "entrance", "legacy-source", "output"}) {
114 if (const auto value = parser.GetString(field); value && value->empty()) {
115 return absl::InvalidArgumentError(std::string("--") + field +
116 " cannot be empty");
117 }
118 }
119 return ParseContext(parser).status();
120}
121
123 Rom* rom, const resources::ArgumentParser& parser,
124 resources::OutputFormatter& formatter) {
126 if (rom != nullptr && !rom->filename().empty()) {
127 context.source_rom_path = rom->filename();
128 context.active_rom_path = rom->filename();
129 }
130 return ExecuteWithContext(rom, parser, formatter, context);
131}
132
134 Rom* /*rom*/, const resources::ArgumentParser& parser,
136 const resources::CommandInvocationContext& invocation) {
139 ASSIGN_OR_RETURN(context, ParseContext(parser));
140 std::shared_ptr<const zelda3::DungeonWallProfileCollection> collection;
142 collection, zelda3::LoadDungeonWallProfiles(*parser.GetString("source")));
144 std::optional<std::string> legacy_path = parser.GetString("legacy-source");
145 if (!legacy_path && project_ != nullptr &&
148 }
149 if (legacy_path) {
151 }
153 zelda3::ResolveWallProfile(*collection, context, legacy));
154
155 std::set<uint32_t> profile_addresses;
156 if (resolved.profile_id()) {
157 for (const auto& profile : collection->profiles()) {
158 if (profile.id != *resolved.profile_id())
159 continue;
160 for (const auto& pattern : profile.patterns) {
161 for (size_t i = 0; i < pattern.words.size(); ++i) {
162 profile_addresses.insert(pattern.pc_offset + i * 2);
163 }
164 }
165 }
166 }
167 const auto output = parser.GetString("output");
168 if (output) {
169 if (resolved.patterns().words.empty()) {
170 return absl::FailedPreconditionError(
171 "No resolved words to emit; retain the existing ROM/project "
172 "fallback");
173 }
174 std::filesystem::path destination;
177 std::vector<std::filesystem::path> inputs{collection->source_path()};
178 if (legacy_path)
179 inputs.push_back(legacy.source_path);
180 if (project_ != nullptr && !project_->filepath.empty()) {
181 inputs.push_back(project_->filepath);
182 }
183 RETURN_IF_ERROR(ProtectOutput(destination, invocation, inputs));
184 RETURN_IF_ERROR(RequireMissing(*output));
185 RETURN_IF_ERROR(RequireMissing(destination));
186 const std::string preview = PreviewAsm(resolved.patterns());
187 auto parsed = zelda3::ParseDungeonTilePatterns(preview);
188 if (!parsed.ok())
189 return parsed.status();
190 if (parsed->words != resolved.patterns().words) {
191 return absl::DataLossError("Preview ASM changed resolved tile words");
192 }
194 .subject = "wall profile preview", .published_file = "preview ASM"};
195 std::unique_ptr<core::SourceArtifactPublicationLock> lock;
197 {destination}, labels, inputs));
198 RETURN_IF_ERROR(ProtectOutput(destination, invocation, inputs));
199 RETURN_IF_ERROR(RequireMissing(destination));
200 std::vector<core::SourceArtifactUpdate> updates{
201 {collection->source_path(), collection->source_text(),
202 collection->source_text()}};
203 if (legacy_path) {
204 updates.push_back(
205 {legacy.source_path, legacy.source_text, legacy.source_text});
206 }
207 updates.push_back({destination, std::nullopt, preview});
209 *lock, std::move(updates),
210 core::ComputeSourceArtifactSha256(collection->source_text()), [&]() {
211 auto readback = zelda3::LoadDungeonTilePatterns(destination.string());
212 if (!readback.ok())
213 return readback.status();
214 if (readback->words != resolved.patterns().words) {
215 return absl::DataLossError("Preview ASM readback differs");
216 }
217 return absl::OkStatus();
218 }));
219 formatter.AddField("output", destination.string());
220 }
221 formatter.AddField("source", collection->source_path());
222 formatter.AddField("source_sha256", core::ComputeSourceArtifactSha256(
223 collection->source_text()));
224 formatter.AddField("selected", resolved.profile_id().has_value());
225 formatter.AddField("selected_profile", resolved.profile_id().value_or(""));
226 formatter.AddField("room", context.room);
227 formatter.AddField("resolved_word_count",
228 static_cast<uint64_t>(resolved.patterns().words.size()));
229 formatter.AddField("profile_word_count",
230 static_cast<uint64_t>(profile_addresses.size()));
231 formatter.AddField("legacy_word_count",
232 static_cast<uint64_t>(resolved.patterns().words.size() -
233 profile_addresses.size()));
234 formatter.AddField("preview_written", output.has_value());
235 formatter.AddField("rom_written", false);
236 formatter.AddField("scope", "preview_overlay_only_not_native_dispatch");
237 formatter.BeginArray("words");
238 for (const auto& [pc, word] : resolved.patterns().words) {
239 formatter.BeginObject();
240 formatter.AddField("pc_offset", static_cast<uint64_t>(pc));
241 formatter.AddField("word", static_cast<int>(word));
242 formatter.AddField("origin",
243 profile_addresses.contains(pc) ? "profile" : "legacy");
244 formatter.EndObject();
245 }
246 formatter.EndArray();
247 return absl::OkStatus();
248}
249
250} // 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:52
auto filename() const
Definition rom.h:219
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.
absl::Status ExecuteWithContext(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter, const resources::CommandInvocationContext &context) override
Execute with immutable, invocation-scoped ROM path identity.
Descriptor Describe() const override
Provide metadata for TUI/help summaries.
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.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
const std::optional< std::string > & profile_id() const
const DungeonTilePatterns & patterns() const
struct destination destination
Room transition destination.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< zelda3::WallProfileContext > ParseContext(const resources::ArgumentParser &parser)
absl::Status ProtectOutput(const std::filesystem::path &output, const resources::CommandInvocationContext &invocation, const std::vector< std::filesystem::path > &inputs)
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)
absl::StatusOr< bool > PathsAlias(const std::filesystem::path &lhs, const std::filesystem::path &rhs)
absl::Status PublishSourceArtifacts(const SourceArtifactPublicationLock &lock, std::vector< SourceArtifactUpdate > updates, std::string_view expected_primary_sha256, SourceArtifactReadbackValidator readback_validator)
std::string ComputeSourceArtifactSha256(std::string_view content)
absl::StatusOr< std::unique_ptr< SourceArtifactPublicationLock > > AcquireSourceArtifactPublicationLock(const std::vector< fs::path > &targets, const SourceArtifactPublisherLabels &labels, const std::vector< fs::path > &read_only_targets)
absl::StatusOr< std::shared_ptr< const DungeonWallProfileCollection > > LoadDungeonWallProfiles(const std::string &path)
absl::StatusOr< ResolvedWallProfile > ResolveWallProfile(const DungeonWallProfileCollection &collection, const WallProfileContext &context, const DungeonTilePatterns &legacy)
absl::StatusOr< DungeonTilePatterns > LoadDungeonTilePatterns(const std::string &path)
absl::StatusOr< DungeonTilePatterns > ParseDungeonTilePatterns(absl::string_view source)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
std::optional< std::filesystem::path > active_rom_path
std::optional< std::filesystem::path > source_rom_path
std::string GetAbsolutePath(const std::string &relative_path) const
Definition project.cc:2675
std::string dungeon_tile_patterns
Definition project.h:325
std::map< uint32_t, uint16_t > words