yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
resource_labels.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <sstream>
7#include <string>
8#include <vector>
9
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_split.h"
12#include "absl/strings/strip.h"
15
16// External declarations for Hyrule Magic sprite names (defined in sprite.cc)
17namespace yaze::zelda3 {
18extern const char* const kSpriteNames[];
19extern const size_t kSpriteNameCount;
20} // namespace yaze::zelda3
21
22namespace yaze {
23namespace zelda3 {
24
25// ============================================================================
26// Resource Type String Conversion
27// ============================================================================
28
30 switch (type) {
32 return "sprite";
34 return "room";
36 return "entrance";
38 return "item";
40 return "overlord";
42 return "overworld_map";
44 return "music";
46 return "graphics";
48 return "room_effect";
50 return "room_tag";
52 return "tile_type";
53 }
54 return "unknown";
55}
56
57ResourceType StringToResourceType(const std::string& type_str) {
58 if (type_str == "sprite")
60 if (type_str == "room")
62 if (type_str == "entrance")
64 if (type_str == "item")
66 if (type_str == "overlord")
68 if (type_str == "overworld_map")
70 if (type_str == "music")
72 if (type_str == "graphics")
74 if (type_str == "room_effect")
76 if (type_str == "room_tag")
78 if (type_str == "tile_type")
80 // Default fallback
82}
83
84namespace {
85
87 int id) {
88 auto lookup = [&](const std::string& key) -> std::string {
89 auto it = labels.find(key);
90 if (it != labels.end() && !it->second.empty()) {
91 return it->second;
92 }
93 return "";
94 };
95
96 // Canonical storage uses decimal keys.
97 if (std::string decimal = lookup(std::to_string(id)); !decimal.empty()) {
98 return decimal;
99 }
100
101 // Compatibility: older Oracle label bundles often use prefixed hex keys.
102 const std::string hex_upper = absl::StrFormat("%X", id);
103 const std::string hex_lower = absl::StrFormat("%x", id);
104 const std::array<std::string, 8> keys = {
105 absl::StrFormat("0x%s", hex_lower), absl::StrFormat("0x%s", hex_upper),
106 absl::StrFormat("0X%s", hex_upper), absl::StrFormat("$%s", hex_upper),
107 absl::StrFormat("0x%02X", id), absl::StrFormat("0x%03X", id),
108 absl::StrFormat("0x%04X", id), absl::StrFormat("$%02X", id),
109 };
110
111 for (const auto& key : keys) {
112 if (std::string label = lookup(key); !label.empty()) {
113 return label;
114 }
115 }
116
117 return "";
118}
119
121 uint8_t id, const ResourceLabelProvider::ProjectLabels* project_labels) {
122 if (project_labels) {
123 const auto it = project_labels->find("item_receipt");
124 if (it != project_labels->end()) {
125 return LookupProjectLabel(it->second, id);
126 }
127 }
128 return "";
129}
130
132 int id) {
133 // The project registry can be loaded independently of a hack manifest in
134 // real Oracle projects (see ProjectPathsTest::
135 // OpenInjectsOracleDungeonRoomLabelsIntoProjectFile). Don't gate registry
136 // lookups on manifest->loaded().
137 if (!manifest || !manifest->HasProjectRegistry()) {
138 return "";
139 }
140
141 for (const auto& dungeon : manifest->project_registry().dungeons) {
142 for (const auto& room : dungeon.rooms) {
143 if (room.id == id && !room.name.empty()) {
144 return room.name;
145 }
146 }
147 }
148 return "";
149}
150
151} // namespace
152
153// ============================================================================
154// Global Provider Instance
155// ============================================================================
156
158 static ResourceLabelProvider instance;
159 return instance;
160}
161
162// ============================================================================
163// ResourceLabelProvider Implementation
164// ============================================================================
165
166std::string ResourceLabelProvider::GetLabel(ResourceType type, int id) const {
167 std::string type_str = ResourceTypeToString(type);
168
169 // 1. Room names in the project registry are canonical for Oracle project
170 // files. Prefer them over merged resource labels so stale legacy
171 // oracle_room_labels.json / manually imported labels cannot mask the current
172 // dungeons.json room names. Explicit user renames outrank the registry.
173 if (type == ResourceType::kRoom) {
174 if (project_labels_) {
175 if (auto it = project_labels_->find(kRoomOverrideLabelType);
176 it != project_labels_->end()) {
177 if (std::string override_label = LookupProjectLabel(it->second, id);
178 !override_label.empty()) {
179 return override_label;
180 }
181 }
182 }
183 if (std::string registry_label =
184 LookupProjectRegistryRoomLabel(hack_manifest_, id);
185 !registry_label.empty()) {
186 return registry_label;
187 }
188 }
189
190 // 2. Check project-specific labels.
191 // Accept decimal keys and prefixed hex keys for compatibility with older
192 // Oracle label bundles.
193 if (project_labels_) {
194 auto type_it = project_labels_->find(type_str);
195 if (type_it != project_labels_->end()) {
196 if (std::string project_label = LookupProjectLabel(type_it->second, id);
197 !project_label.empty()) {
198 return project_label;
199 }
200 }
201 }
202
203 // 3. Check Hack Manifest for ASM-defined labels
205 if (type == ResourceType::kRoomTag) {
206 std::string manifest_label = hack_manifest_->GetRoomTagLabel(id);
207 if (!manifest_label.empty()) {
208 return manifest_label;
209 }
210 }
211 // Future: Add message label lookup if we add GetMessageLabel to HackManifest
212 }
213
214 // 4-5. Hyrule Magic (sprites, when preferred), then vanilla.
215 return GetDefaultLabel(type, id);
216}
217
219 int id) const {
220 if (type == ResourceType::kSprite && prefer_hmagic_) {
221 std::string hmagic = GetHMagicLabel(type, id);
222 if (!hmagic.empty()) {
223 return hmagic;
224 }
225 }
226 return GetVanillaLabel(type, id);
227}
228
230 int id) const {
231 ResolvedLabel resolved;
232 resolved.name = GetLabel(type, id);
233 resolved.default_name = GetDefaultLabel(type, id);
234 // Projects saved with embedded labels repeat vanilla names; those are not
235 // renames even when Hyrule Magic names are the default.
236 resolved.from_project = resolved.name != resolved.default_name &&
237 resolved.name != GetVanillaLabel(type, id);
238 return resolved;
239}
240
241std::string ResolvedLabel::Display() const {
242 if (!from_project) {
243 return name;
244 }
245 return absl::StrFormat("%s (vanilla: %s)", name, default_name);
246}
247
248std::string ResourceLabelProvider::GetLabel(const std::string& type_str,
249 int id) const {
250 ResourceType type = StringToResourceType(type_str);
251 return GetLabel(type, id);
252}
253
255 const std::string& label) {
256 if (!project_labels_) {
257 return;
258 }
259 std::string type_str = ResourceTypeToString(type);
260 (*project_labels_)[type_str][std::to_string(id)] = label;
261}
262
264 if (!project_labels_) {
265 return;
266 }
267 std::string type_str = ResourceTypeToString(type);
268 auto type_it = project_labels_->find(type_str);
269 if (type_it != project_labels_->end()) {
270 type_it->second.erase(std::to_string(id));
271 }
272}
273
275 if (!project_labels_) {
276 return false;
277 }
278 std::string type_str = ResourceTypeToString(type);
279 auto type_it = project_labels_->find(type_str);
280 if (type_it == project_labels_->end()) {
281 return false;
282 }
283 return !LookupProjectLabel(type_it->second, id).empty();
284}
285
287 int id) const {
288 switch (type) {
290 const auto& names = Zelda3Labels::GetSpriteNames();
291 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
292 return names[id];
293 }
294 return absl::StrFormat("Sprite %02X", id);
295 }
296 case ResourceType::kRoom: {
297 const auto& names = Zelda3Labels::GetRoomNames();
298 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
299 return names[id];
300 }
301 return absl::StrFormat("Room %03X", id);
302 }
304 const auto& names = Zelda3Labels::GetEntranceNames();
305 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
306 return names[id];
307 }
308 return absl::StrFormat("Entrance %02X", id);
309 }
310 case ResourceType::kItem: {
311 const auto& names = Zelda3Labels::GetItemNames();
312 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
313 return names[id];
314 }
315 return absl::StrFormat("Item %02X", id);
316 }
318 const auto& names = Zelda3Labels::GetOverlordNames();
319 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
320 return names[id];
321 }
322 return absl::StrFormat("Overlord %02X", id);
323 }
325 const auto& names = Zelda3Labels::GetOverworldMapNames();
326 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
327 return names[id];
328 }
329 return absl::StrFormat("Map %02X", id);
330 }
332 const auto& names = Zelda3Labels::GetMusicTrackNames();
333 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
334 return names[id];
335 }
336 return absl::StrFormat("Music %02X", id);
337 }
339 const auto& names = Zelda3Labels::GetGraphicsSheetNames();
340 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
341 return names[id];
342 }
343 return absl::StrFormat("GFX %02X", id);
344 }
346 const auto& names = Zelda3Labels::GetRoomEffectNames();
347 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
348 return names[id];
349 }
350 return absl::StrFormat("Effect %02X", id);
351 }
353 const auto& names = Zelda3Labels::GetRoomTagNames();
354 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
355 return names[id];
356 }
357 return absl::StrFormat("Tag %02X", id);
358 }
360 const auto& names = Zelda3Labels::GetTileTypeNames();
361 if (id >= 0 && static_cast<size_t>(id) < names.size()) {
362 return names[id];
363 }
364 return absl::StrFormat("Tile %02X", id);
365 }
366 }
367 return absl::StrFormat("Unknown %02X", id);
368}
369
371 int id) const {
372 // Hyrule Magic names are only available for sprites
373 if (type != ResourceType::kSprite) {
374 return "";
375 }
376
377 // Use the kSpriteNames array from sprite_names.h
378 if (id >= 0 && static_cast<size_t>(id) < kSpriteNameCount) {
379 return kSpriteNames[id];
380 }
381 return "";
382}
383
385 switch (type) {
387 return 256;
389 return 297;
391 return 133;
393 return static_cast<int>(Zelda3Labels::GetItemNames().size());
395 return static_cast<int>(Zelda3Labels::GetOverlordNames().size());
397 return 160;
399 return static_cast<int>(Zelda3Labels::GetMusicTrackNames().size());
401 return static_cast<int>(Zelda3Labels::GetGraphicsSheetNames().size());
403 return static_cast<int>(Zelda3Labels::GetRoomEffectNames().size());
405 return static_cast<int>(Zelda3Labels::GetRoomTagNames().size());
407 return static_cast<int>(Zelda3Labels::GetTileTypeNames().size());
408 }
409 return 0;
410}
411
414 if (!project_labels_) {
415 return nullptr;
416 }
417 std::string type_str = ResourceTypeToString(type);
418 auto it = project_labels_->find(type_str);
419 if (it == project_labels_->end()) {
420 return nullptr;
421 }
422 return &it->second;
423}
424
430
431// ============================================================================
432// ZScream DefaultNames.txt Import/Export
433// ============================================================================
434
436 const std::string& content) {
437 if (!project_labels_) {
438 return absl::FailedPreconditionError(
439 "Project labels not initialized. Open a project first.");
440 }
441
442 std::istringstream stream(content);
443 std::string line;
444 std::string current_section;
445 int line_index = 0;
446 int line_number = 0;
447
448 while (std::getline(stream, line)) {
449 line_number++;
450
451 // Trim whitespace
452 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
453
454 // Skip empty lines and comments
455 if (trimmed.empty() || trimmed.substr(0, 2) == "//") {
456 continue;
457 }
458
459 // Check for section headers
460 if (trimmed.front() == '[' && trimmed.back() == ']') {
461 current_section = trimmed.substr(1, trimmed.length() - 2);
462 line_index = 0; // Reset line index for new section
463 continue;
464 }
465
466 // Parse the line based on current section
467 if (!current_section.empty()) {
468 ParseZScreamLine(trimmed, current_section, line_index);
469 }
470 }
471
472 return absl::OkStatus();
473}
474
475bool ResourceLabelProvider::ParseZScreamLine(const std::string& line,
476 const std::string& section,
477 int& line_index) {
478 if (!project_labels_) {
479 return false;
480 }
481
482 // Determine resource type and format based on section
483 std::string resource_type;
484 bool has_hex_prefix = false;
485
486 if (section == "Sprites Names") {
487 resource_type = "sprite";
488 has_hex_prefix = true; // Format: "00 Raven"
489 } else if (section == "Rooms Names") {
490 resource_type = "room";
491 has_hex_prefix = false; // Format: just "Room Name" by line order
492 } else if (section == "Chests Items") {
493 resource_type = "item_receipt";
494 has_hex_prefix = true; // Format: "00 Fighter sword and shield"
495 } else if (section == "Tags Names") {
496 resource_type = "room_tag";
497 has_hex_prefix = false; // Format: just "Tag Name" by line order
498 } else {
499 // Unknown section, skip
500 return false;
501 }
502
503 std::string id_str;
504 std::string label;
505
506 if (has_hex_prefix) {
507 // Format: "XX Label Name" where XX is hex
508 size_t space_pos = line.find(' ');
509 if (space_pos == std::string::npos || space_pos < 2) {
510 // No space found or too short for hex prefix
511 return false;
512 }
513
514 std::string hex_str = line.substr(0, space_pos);
515 label = line.substr(space_pos + 1);
516
517 // Parse hex to int
518 try {
519 int id = std::stoi(hex_str, nullptr, 16);
520 id_str = std::to_string(id);
521 } catch (...) {
522 return false;
523 }
524 } else {
525 // Line index determines the ID
526 id_str = std::to_string(line_index);
527 label = line;
528 line_index++;
529 }
530
531 // Trim the label
532 label = std::string(absl::StripAsciiWhitespace(label));
533
534 // Store the label
535 if (!label.empty()) {
536 (*project_labels_)[resource_type][id_str] = label;
537 }
538
539 return true;
540}
541
543 std::ostringstream output;
544
545 output << "//Do not use brackets [] in naming\n";
546
547 // Export sprites
548 output << "[Sprites Names]\n";
549 for (int i = 0; i < 256; ++i) {
550 std::string label = GetLabel(ResourceType::kSprite, i);
551 output << absl::StrFormat("%02X %s\n", i, label);
552 }
553
554 // Export rooms
555 output << "\n[Rooms Names]\n";
556 for (int i = 0; i < 297; ++i) {
557 std::string label = GetLabel(ResourceType::kRoom, i);
558 output << label << "\n";
559 }
560
561 // ZScream chest IDs index the receipt table, not the generic item catalog.
562 output << "\n[Chests Items]\n";
563 for (int i = 0; i <= 0xFF; ++i) {
564 const auto id = static_cast<uint8_t>(i);
565 if (i >= static_cast<int>(Zelda3Labels::GetItemReceiptNames().size()) &&
566 LookupProjectReceiptLabel(id, project_labels_).empty()) {
567 continue;
568 }
569 output << absl::StrFormat("%02X %s\n", i,
571 }
572
573 // Export room tags
574 output << "\n[Tags Names]\n";
576 for (int i = 0; i < tag_count; ++i) {
577 std::string label = GetLabel(ResourceType::kRoomTag, i);
578 output << label << "\n";
579 }
580
581 return output.str();
582}
583
584// ============================================================================
585// Oracle of Secrets Sprite Registry Import
586// ============================================================================
587
589 const std::string& csv_content) {
590 // Create local storage if project_labels_ not set
591 static ProjectLabels local_labels;
592 if (!project_labels_) {
593 project_labels_ = &local_labels;
594 }
595
596 std::istringstream stream(csv_content);
597 std::string line;
598 int line_number = 0;
599 int imported_count = 0;
600
601 while (std::getline(stream, line)) {
602 line_number++;
603
604 // Trim whitespace
605 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
606
607 // Skip empty lines and header row
608 if (trimmed.empty() || line_number == 1) {
609 continue;
610 }
611
612 // Parse CSV: name,id,paths,group,notes,allow_dupe
613 std::vector<std::string> fields = absl::StrSplit(trimmed, ',');
614 if (fields.size() < 2) {
615 continue; // Need at least name and id
616 }
617
618 std::string name = std::string(absl::StripAsciiWhitespace(fields[0]));
619 std::string id_str = std::string(absl::StripAsciiWhitespace(fields[1]));
620
621 // Parse sprite ID (format: $XX or 0xXX or just XX)
622 int sprite_id = -1;
623 if (id_str.empty()) {
624 continue;
625 }
626
627 // Handle $XX format
628 if (id_str[0] == '$') {
629 id_str = id_str.substr(1);
630 }
631 // Handle 0xXX format
632 if (id_str.size() >= 2 && id_str[0] == '0' &&
633 (id_str[1] == 'x' || id_str[1] == 'X')) {
634 id_str = id_str.substr(2);
635 }
636
637 try {
638 sprite_id = std::stoi(id_str, nullptr, 16);
639 } catch (...) {
640 continue; // Skip invalid IDs
641 }
642
643 if (sprite_id < 0 || sprite_id > 255) {
644 continue; // Invalid sprite ID range
645 }
646
647 // Store the sprite name
648 (*project_labels_)["sprite"][std::to_string(sprite_id)] = name;
649 imported_count++;
650 }
651
652 if (imported_count == 0) {
653 return absl::InvalidArgumentError(
654 "No valid sprite entries found in registry CSV");
655 }
656
657 return absl::OkStatus();
658}
659
661 uint8_t id, const ResourceLabelProvider::ProjectLabels* project_labels) {
662 if (const auto label = LookupProjectReceiptLabel(id, project_labels);
663 !label.empty()) {
664 return label;
665 }
666 const auto& names = Zelda3Labels::GetItemReceiptNames();
667 if (id < names.size())
668 return names[id];
669 return absl::StrFormat("Unknown item %02X", id);
670}
671
673 uint8_t id, const ResourceLabelProvider::ProjectLabels* project_labels) {
674 if (!LookupProjectReceiptLabel(id, project_labels).empty())
675 return "project";
676 return id < Zelda3Labels::GetItemReceiptNames().size() ? "vanilla_receipt"
677 : "unknown";
678}
679
680std::string GetItemReceiptLabel(uint8_t id) {
681 return GetItemReceiptLabel(id, GetResourceLabels().GetAllProjectLabels());
682}
683
684const char* GetItemReceiptLabelSource(uint8_t id) {
686 GetResourceLabels().GetAllProjectLabels());
687}
688
689} // namespace zelda3
690} // namespace yaze
Loads and queries the hack manifest JSON for yaze-ASM integration.
const ProjectRegistry & project_registry() const
bool HasProjectRegistry() const
std::string GetRoomTagLabel(uint8_t tag_id) const
Get the human-readable label for a room tag ID.
bool loaded() const
Check if the manifest has been loaded.
Unified interface for accessing resource labels with project overrides.
std::string ExportToZScreamFormat() const
Export project labels to ZScream DefaultNames.txt format.
void SetProjectLabel(ResourceType type, int id, const std::string &label)
Set a project-specific label override.
bool HasProjectLabel(ResourceType type, int id) const
Check if a project-specific label exists.
void ClearAllProjectLabels()
Clear all project labels.
int GetResourceCount(ResourceType type) const
Get the count of resources for a given type.
std::unordered_map< std::string, std::string > LabelMap
const core::HackManifest * hack_manifest_
std::string GetDefaultLabel(ResourceType type, int id) const
Get the label without project overrides (steps 4-5 above)
ResolvedLabel ResolveLabel(ResourceType type, int id) const
Get the label and the default name it replaces.
std::string GetLabel(ResourceType type, int id) const
Get a label for a resource by type and ID.
bool ParseZScreamLine(const std::string &line, const std::string &section, int &line_index)
std::unordered_map< std::string, LabelMap > ProjectLabels
absl::Status ImportFromZScreamFormat(const std::string &content)
Import labels from ZScream DefaultNames.txt format.
std::string GetVanillaLabel(ResourceType type, int id) const
Get the vanilla (default) label for a resource.
const LabelMap * GetProjectLabelsForType(ResourceType type) const
Get all project labels for a given type.
std::string GetHMagicLabel(ResourceType type, int id) const
Get the Hyrule Magic label for a resource (sprites only)
void ClearProjectLabel(ResourceType type, int id)
Clear a project-specific label (revert to default)
absl::Status ImportOracleSpriteRegistry(const std::string &csv_content)
Import sprite labels from Oracle of Secrets registry.csv format.
std::string LookupProjectRegistryRoomLabel(const core::HackManifest *manifest, int id)
std::string LookupProjectReceiptLabel(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
std::string LookupProjectLabel(const ResourceLabelProvider::LabelMap &labels, int id)
Zelda 3 specific classes and functions.
ResourceType
Enumeration of all supported resource types for labeling.
const char * GetItemReceiptLabelSource(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
const size_t kSpriteNameCount
Definition sprite.cc:296
const char *const kSpriteNames[]
Definition sprite.cc:295
std::string GetItemReceiptLabel(uint8_t id, const ResourceLabelProvider::ProjectLabels *project_labels)
std::string ResourceTypeToString(ResourceType type)
Convert ResourceType enum to string key for storage.
ResourceType StringToResourceType(const std::string &type_str)
Convert string key to ResourceType enum.
ResourceLabelProvider & GetResourceLabels()
Get the global ResourceLabelProvider instance.
std::vector< DungeonEntry > dungeons
A label plus the name it replaces.
static const std::vector< std::string > & GetRoomNames()
static const std::vector< std::string > & GetItemNames()
static const std::vector< std::string > & GetEntranceNames()
static const std::vector< std::string > & GetGraphicsSheetNames()
static const std::vector< std::string > & GetMusicTrackNames()
static const std::vector< std::string > & GetItemReceiptNames()
static const std::vector< std::string > & GetTileTypeNames()
static const std::vector< std::string > & GetOverlordNames()
static const std::vector< std::string > & GetSpriteNames()
static const std::vector< std::string > & GetOverworldMapNames()
static const std::vector< std::string > & GetRoomEffectNames()
static const std::vector< std::string > & GetRoomTagNames()