yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_census_commands.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <optional>
7
8#include "absl/strings/ascii.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_join.h"
11#include "cli/util/hex_util.h"
12#include "core/hack_manifest.h"
13#include "core/project.h"
14#include "rom/rom.h"
15#include "util/macro.h"
19
20namespace yaze::cli::handlers {
21
22namespace {
23
24namespace fs = std::filesystem;
25
26// YazeProject::Open installs its manifest in the global label provider;
27// restore the previous binding when done.
29 public:
31 : provider_(provider), previous_(provider.hack_manifest()) {}
32 ~ScopedLabelManifestRestore() { provider_.SetHackManifest(previous_); }
35 delete;
36
37 private:
39 const core::HackManifest* previous_ = nullptr;
40};
41
42absl::Status LoadProjectFolder(const fs::path& folder,
43 core::HackManifest* manifest) {
44 // Manifest first: LoadFromFile resets the registry.
45 for (const fs::path candidate : {folder / "Roms" / "hack_manifest.json",
46 folder / "hack_manifest.json"}) {
47 std::error_code ec;
48 if (fs::exists(candidate, ec)) {
49 auto status = manifest->LoadFromFile(candidate.string());
50 if (!status.ok()) {
51 return status;
52 }
53 break;
54 }
55 }
56 return manifest->LoadProjectRegistry(folder.string());
57}
58
59// A .yaze (or .yazeproj) file goes through YazeProject::Open, which looks for
60// the registry in code_folder, then beside the hack manifest, then beside the
61// project file. Oracle's project has code_folder=Core while the registry is
62// <repo>/Docs/Dev/Planning/, so only the fallbacks find it.
63absl::Status LoadProjectFile(const std::string& project_arg,
64 core::HackManifest* manifest) {
66 absl::Status status;
67 {
69 try {
70 status = project.Open(project_arg);
71 } catch (const std::exception& error) {
72 return absl::InvalidArgumentError(absl::StrFormat(
73 "Cannot load project '%s': %s", project_arg, error.what()));
74 }
75 }
76 if (!status.ok()) {
77 return status;
78 }
79 *manifest = project.hack_manifest;
80 return absl::OkStatus();
81}
82
83// --project accepts the project's folder (with Docs/Dev/Planning/
84// dungeons.json and Roms/hack_manifest.json) or a .yaze project file. Either
85// way the registry must load: without it the census silently derives owners
86// from entrances and reports different counts.
87absl::Status LoadProjectData(const std::string& project_arg,
88 core::HackManifest* manifest) {
89 const fs::path path(project_arg);
90 std::error_code ec;
91 if (!fs::exists(path, ec)) {
92 return absl::NotFoundError(
93 absl::StrFormat("--project path does not exist: %s", project_arg));
94 }
95 const bool is_folder =
96 fs::is_directory(path, ec) && path.extension() != ".yazeproj";
97 RETURN_IF_ERROR(is_folder ? LoadProjectFolder(path, manifest)
98 : LoadProjectFile(project_arg, manifest));
99 if (manifest->project_registry().dungeons.empty()) {
100 return absl::FailedPreconditionError(absl::StrFormat(
101 "--project %s: no dungeon registry found (Docs/Dev/Planning/"
102 "dungeons.json under %s). Owners would fall back to entrance dungeon "
103 "IDs; pass the folder that holds Docs/Dev/Planning/dungeons.json, or "
104 "drop --project for a ROM-only census",
105 project_arg,
106 is_folder ? "that folder"
107 : "the project's code_folder, the hack manifest's folder "
108 "or the project file's folder"));
109 }
110 return absl::OkStatus();
111}
112
113std::string Hex(int room_id) {
114 return absl::StrFormat("0x%02X", room_id);
115}
116
117std::string OwnerId(const zelda3::RoomCensus& census,
118 const zelda3::RoomCensusEntry& entry) {
119 return entry.owner_index >= 0 ? census.owners[entry.owner_index].id : "-";
120}
121
122std::string FormatTable(const zelda3::RoomCensus& census,
123 const std::vector<int>& rooms) {
124 std::string out;
125 out += absl::StrFormat(
126 "Room census: %d free, %d reclaimable, %d in use (%d orphan), %d "
127 "interior\n",
128 census.free_count, census.reclaimable_count, census.in_use_count,
129 census.orphan_count, census.interior_count);
130 out += absl::StrFormat("Owners: %s\n", census.owners_from_project
131 ? "project dungeons.json"
132 : "derived from entrances");
133 out += absl::StrFormat("Vanilla baseline: %s\n",
136 : "none (reclaimable needs one)");
137 if (!census.free_clusters.empty()) {
138 out += absl::StrFormat("Largest free block: %s\n",
139 census.free_clusters.front().summary);
140 }
141 out += absl::StrFormat("%-6s %-12s %-6s %s\n", "ROOM", "STATUS", "OWNER",
142 "REASONS");
143 for (int room_id : rooms) {
144 const auto& entry = census.rooms[room_id];
145 std::string status = zelda3::RoomCensusStatusName(entry.status);
146 if (entry.orphan) {
147 status += "*";
148 }
149 out += absl::StrFormat("%-6s %-12s %-6s %s\n", Hex(room_id), status,
150 OwnerId(census, entry),
151 absl::StrJoin(entry.reasons, " | "));
152 }
153 out += "(* = orphan: in use, but nothing reaches it)\n";
154 return out;
155}
156
157} // namespace
158
159absl::Status LoadRoomCensusProject(const std::string& project_arg,
160 core::HackManifest* manifest) {
161 return LoadProjectData(project_arg, manifest);
162}
163
165 const resources::ArgumentParser& parser) {
166 if (auto status = parser.GetString("status"); status.has_value()) {
167 const std::string lowered = absl::AsciiStrToLower(*status);
168 if (lowered != "free" && lowered != "reclaimable" && lowered != "in_use") {
169 return absl::InvalidArgumentError(
170 "--status must be free, reclaimable or in_use");
171 }
172 }
173 return absl::OkStatus();
174}
175
177 Rom* rom, const resources::ArgumentParser& parser,
178 resources::OutputFormatter& formatter) {
179 // Generator for the built-in vanilla fingerprint table.
180 if (auto out_path = parser.GetString("emit-vanilla-fingerprints");
181 out_path.has_value()) {
182 auto fingerprints = zelda3::ComputeRoomFingerprints(rom);
183 if (!fingerprints.ok()) {
184 return fingerprints.status();
185 }
186 std::ofstream file(*out_path, std::ios::binary | std::ios::trunc);
187 if (!file) {
188 return absl::PermissionDeniedError(
189 absl::StrFormat("Cannot write %s", *out_path));
190 }
192 *fingerprints,
193 absl::StrFormat("%s (%d bytes)",
194 fs::path(rom->filename()).filename().string(),
195 static_cast<int>(rom->size())));
196 formatter.AddField("status", "success");
197 formatter.AddField("written", *out_path);
198 return absl::OkStatus();
199 }
200
202 core::HackManifest manifest;
203 if (auto project_arg = parser.GetString("project"); project_arg.has_value()) {
204 if (auto status = LoadRoomCensusProject(*project_arg, &manifest);
205 !status.ok()) {
206 return status;
207 }
208 options.project = &manifest.project_registry();
209 options.manifest = &manifest;
210 }
211 Rom vanilla_rom;
212 if (auto vanilla_path = parser.GetString("vanilla");
213 vanilla_path.has_value()) {
214 auto status = vanilla_rom.LoadFromFile(*vanilla_path);
215 if (!status.ok()) {
216 return status;
217 }
218 options.vanilla_rom = &vanilla_rom;
219 }
220
221 auto census_or = zelda3::ComputeRoomCensus(rom, options);
222 if (!census_or.ok()) {
223 return census_or.status();
224 }
225 const zelda3::RoomCensus& census = *census_or;
226
227 std::vector<int> rooms;
228 std::optional<zelda3::RoomCensusStatus> status_filter;
229 if (auto status = parser.GetString("status"); status.has_value()) {
230 const std::string lowered = absl::AsciiStrToLower(*status);
231 status_filter = lowered == "free" ? zelda3::RoomCensusStatus::kFree
232 : lowered == "reclaimable"
235 }
236 if (auto room_arg = parser.GetString("room"); room_arg.has_value()) {
237 int room_id = -1;
238 if (!util::ParseHexString(*room_arg, &room_id) || room_id < 0 ||
239 room_id >= zelda3::kRoomCensusRoomCount) {
240 return absl::InvalidArgumentError(
241 "--room must be a hex room id 0x000-0x127");
242 }
243 rooms.push_back(room_id);
244 } else {
245 for (const auto& entry : census.rooms) {
246 if (!status_filter.has_value() || entry.status == *status_filter) {
247 rooms.push_back(entry.room_id);
248 }
249 }
250 }
251
252 const std::string format =
253 absl::AsciiStrToLower(parser.GetString("format").value_or("json"));
254 if (format == "table" || format == "text") {
255 formatter.AddRawText(FormatTable(census, rooms));
256 return absl::OkStatus();
257 }
258
259 formatter.AddField("rom", fs::path(rom->filename()).filename().string());
260 formatter.AddField("owners_source", census.owners_from_project
261 ? "project"
262 : "derived_from_entrances");
263 formatter.AddField("vanilla_baseline", census.has_vanilla_baseline
265 : "none");
266 formatter.AddField("expanded_entrance_tables",
268 formatter.BeginObject("counts");
269 formatter.AddField("free", census.free_count);
270 formatter.AddField("reclaimable", census.reclaimable_count);
271 formatter.AddField("in_use", census.in_use_count);
272 formatter.AddField("orphan", census.orphan_count);
273 formatter.AddField("interior", census.interior_count);
274 formatter.EndObject();
275
276 auto add_status_list = [&](const char* key, auto predicate) {
277 formatter.BeginArray(key);
278 for (const auto& entry : census.rooms) {
279 if (predicate(entry)) {
280 formatter.AddArrayItem(Hex(entry.room_id));
281 }
282 }
283 formatter.EndArray();
284 };
285 add_status_list("free_rooms", [](const auto& e) {
286 return e.status == zelda3::RoomCensusStatus::kFree;
287 });
288 add_status_list("reclaimable_rooms", [](const auto& e) {
290 });
291 add_status_list("orphan_rooms", [](const auto& e) { return e.orphan; });
292
293 formatter.AddField("largest_free_block",
294 census.free_clusters.empty()
295 ? std::string()
296 : census.free_clusters.front().summary);
297 formatter.BeginArray("free_blocks");
298 for (const auto& cluster : census.free_clusters) {
299 formatter.BeginObject();
300 formatter.AddField("summary", cluster.summary);
301 formatter.BeginArray("rooms");
302 for (int room : cluster.rooms) {
303 formatter.AddArrayItem(Hex(room));
304 }
305 formatter.EndArray();
306 formatter.BeginArray("contiguous_rooms");
307 for (int room : cluster.core_rooms) {
308 formatter.AddArrayItem(Hex(room));
309 }
310 formatter.EndArray();
311 formatter.EndObject();
312 }
313 formatter.EndArray();
314
315 formatter.BeginArray("owners");
316 for (const auto& owner : census.owners) {
317 formatter.BeginObject();
318 formatter.AddField("id", owner.id);
319 formatter.AddField("name", owner.name);
320 formatter.AddField("interior", owner.interior);
321 formatter.AddField("from_project", owner.from_project);
322 formatter.AddField("rooms", owner.room_count);
323 formatter.EndObject();
324 }
325 formatter.EndArray();
326
327 formatter.BeginArray("rooms");
328 for (int room_id : rooms) {
329 const auto& entry = census.rooms[room_id];
330 formatter.BeginObject();
331 formatter.AddField("room", Hex(room_id));
332 formatter.AddField("status", zelda3::RoomCensusStatusName(entry.status));
333 formatter.AddField("owner", OwnerId(census, entry));
334 formatter.AddField("owner_name", entry.owner_index >= 0
335 ? census.owners[entry.owner_index].name
336 : std::string());
337 formatter.AddField("interior", entry.interior);
338 formatter.AddField("reached", entry.reached);
339 formatter.AddField("empty", entry.empty);
340 formatter.AddField("orphan", entry.orphan);
341 formatter.AddField("objects", entry.object_count);
342 formatter.AddField("sprites", entry.sprite_count);
343 formatter.AddRawJsonField(
344 "vanilla_similarity",
345 entry.vanilla_similarity < 0.0f
346 ? "null"
347 : absl::StrFormat("%.2f", entry.vanilla_similarity));
348 formatter.BeginArray("reasons");
349 for (const auto& reason : entry.reasons) {
350 formatter.AddArrayItem(reason);
351 }
352 formatter.EndArray();
353 formatter.BeginArray("references");
354 for (const auto& ref : entry.references) {
355 formatter.BeginObject();
356 formatter.AddField("kind", zelda3::RoomReferenceKindName(ref.kind));
357 formatter.AddField("strong", ref.strong);
358 if (ref.from_room >= 0) {
359 formatter.AddField("from", Hex(ref.from_room));
360 }
361 if (ref.entrance_id >= 0) {
362 formatter.AddHexField("entrance", ref.entrance_id, 2);
363 }
364 formatter.AddField("detail", ref.detail);
365 formatter.EndObject();
366 }
367 formatter.EndArray();
368 formatter.EndObject();
369 }
370 formatter.EndArray();
371 return absl::OkStatus();
372}
373
374} // 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
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:491
auto filename() const
Definition rom.h:207
auto size() const
Definition rom.h:200
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.
ScopedLabelManifestRestore & operator=(const ScopedLabelManifestRestore &)=delete
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)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddRawText(const std::string &text)
Append preformatted text (for example an aligned table). Ignored in JSON mode.
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.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
Loads and queries the hack manifest JSON for yaze-ASM integration.
const ProjectRegistry & project_registry() const
absl::Status LoadProjectRegistry(const std::string &code_folder)
Load project registry data from the code folder.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
Unified interface for accessing resource labels with project overrides.
std::string FormatTable(const zelda3::RoomCensus &census, const std::vector< int > &rooms)
absl::Status LoadProjectData(const std::string &project_arg, core::HackManifest *manifest)
absl::Status LoadProjectFile(const std::string &project_arg, core::HackManifest *manifest)
absl::Status LoadProjectFolder(const fs::path &folder, core::HackManifest *manifest)
std::string OwnerId(const zelda3::RoomCensus &census, const zelda3::RoomCensusEntry &entry)
absl::Status LoadRoomCensusProject(const std::string &project_arg, core::HackManifest *manifest)
const char * RoomReferenceKindName(RoomReferenceKind kind)
absl::StatusOr< RoomCensus > ComputeRoomCensus(Rom *rom, const RoomCensusOptions &options)
const char * RoomCensusStatusName(RoomCensusStatus status)
absl::StatusOr< std::vector< std::vector< uint16_t > > > ComputeRoomFingerprints(Rom *rom, util::StopToken stop_token)
std::string FormatVanillaFingerprintTable(const std::vector< std::vector< uint16_t > > &fingerprints, const std::string &source_note)
constexpr int kRoomCensusRoomCount
Definition room_census.h:55
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< DungeonEntry > dungeons
Modern project structure with comprehensive settings consolidation.
Definition project.h:287
core::HackManifest hack_manifest
Definition project.h:360
absl::Status Open(const std::string &project_path)
Definition project.cc:537
const core::ProjectRegistry * project
const core::HackManifest * manifest
std::vector< RoomCensusOwner > owners
std::vector< RoomCensusCluster > free_clusters
std::string vanilla_baseline_source
std::vector< RoomCensusEntry > rooms